Aspects: When Behavior Cuts Across
Some behaviors don't fit neatly into functions or objects. They cut across the entire system:
- Logging---record what happens everywhere
- Security---check permissions before every operation
- Transactions---wrap database operations atomically
- Change tracking---notice when any field changes
Without special support, we'd scatter this logic everywhere:
function moveKing(game, from, to):
log("moveKing called with " + from + " -> " + to)
if not hasPermission(currentUser, "move"):
throw "Not authorized"
beginTransaction()
trackChange(game, "pieces", game.pieces)
// Finally, the actual logic:
piece = game.pieces[from]
delete game.pieces[from]
piece.hasMoved = true
game.pieces[to] = piece
game.turn = oppositeColor(piece.color)
game.moveCount = game.moveCount + 1
commitTransaction()
log("moveKing completed successfully")The actual business logic---five lines---drowns in infrastructure. And this pattern repeats in every function.
The Cross-Cutting Problem
Imagine adding logging to a chess game. You want to log every function call. With traditional approaches:
function movePiece(game, from, to):
log("movePiece called")
// ... actual logic ...
log("movePiece completed")
function capturePiece(game, square):
log("capturePiece called")
// ... actual logic ...
log("capturePiece completed")
function castle(game, side):
log("castle called")
// ... actual logic ...
log("castle completed")
// ... repeat for every function ...The logging is:
- Scattered: Spread across every function
- Tangled: Mixed with business logic
- Hard to change: Updating requires touching every function
- Hard to disable: No single switch to turn it off
Aspect-Oriented Programming
Aspect-Oriented Programming (AOP) separates these concerns. We define the cross-cutting behavior once, then "weave" it into the code:
// Define the aspect once
aspect Logging:
before any function call:
log(function.name + " called with " + arguments)
after any function call:
log(function.name + " completed")
// The actual functions stay clean
function movePiece(game, from, to):
piece = game.pieces[from]
delete game.pieces[from]
game.pieces[to] = piece
function capturePiece(game, square):
captured = game.pieces[square]
delete game.pieces[square]
return capturedThe aspect defines:
- When to act (before/after/around function calls)
- Where to act (which functions, which patterns)
- What to do (the cross-cutting behavior)
Join Points and Pointcuts
AOP has its own vocabulary:
Join point: A place where aspect code can be inserted. Function calls, field accesses, exception throws.
Pointcut: A pattern that selects join points. "All functions starting with 'move'\,", "All field writes on Game objects."
Advice: The code to run at selected join points. Before, after, or around the original code.
// Pointcut: functions matching "move*"
pointcut moveFunctions = execution(* move*(..))
// Advice: what to do at those points
before moveFunctions:
log("A move is starting")
after moveFunctions:
log("A move completed")Types of Advice
Before advice: Runs before the original code.
aspect Security:
before any function matching "move*":
if not hasPermission(currentUser, "move"):
throw "Not authorized"After advice: Runs after the original code.
aspect Notification:
after any function matching "*Piece":
notifyObservers("piece changed")Around advice: Wraps the original code, controlling whether it runs.
aspect Caching:
around any function matching "calculate*":
key = cacheKey(arguments)
if cache.has(key):
return cache.get(key)
result = proceed() // run the original function
cache.set(key, result)
return resultChange Tracking as an Aspect
Remember our change tracking from earlier chapters? It's a perfect fit for aspects:
aspect ChangeTracking:
around any function matching "*":
before = snapshot(arguments[0]) // first arg is game state
result = proceed() // run the actual function
after = snapshot(arguments[0])
changes = diff(before, after)
if changes.length > 0:
recordChanges(changes)
return resultNow every function that modifies game state is automatically tracked---without modifying any of them.
Weaving
The "weaving" process combines aspects with the base code. This can happen:
At compile time: A tool modifies the source or bytecode before execution.
// Original source
function movePiece(game, from, to): ...
// After weaving (generated code)
function movePiece(game, from, to):
log("movePiece called") // injected
checkPermission("move") // injected
... original logic ...
log("movePiece completed") // injectedAt runtime: A framework intercepts calls dynamically.
// Proxy-based weaving
movePiece = createProxy(originalMovePiece, {
apply(target, thisArg, args):
runBeforeAdvice()
result = target.apply(thisArg, args)
runAfterAdvice()
return result
})Real-World Aspects
Transaction Management
aspect Transactions:
around any function annotated with @Transactional:
beginTransaction()
try:
result = proceed()
commitTransaction()
return result
catch error:
rollbackTransaction()
throw errorPerformance Monitoring
aspect Performance:
around any function:
start = now()
result = proceed()
duration = now() - start
if duration > threshold:
warn(function.name + " took " + duration + "ms")
metrics.record(function.name, duration)
return resultRetry Logic
aspect Retry:
around any function annotated with @Retryable:
attempts = 0
maxAttempts = 3
while attempts < maxAttempts:
try:
return proceed()
catch TransientError:
attempts = attempts + 1
wait(exponentialBackoff(attempts))
throw "Max retries exceeded"The Separation of Concerns
AOP achieves separation at a new level. Traditional modularity separates:
AOP separates:
- Data from behavior (objects)
- Interface from implementation (abstraction)
- What from how (declarative programming)
- Core logic from cross-cutting concerns
- Business behavior from infrastructure
- What the code does from what should happen around it
The Dark Side of Aspects
Aspects are powerful---and that power has costs:
Invisible behavior: Code appears to do one thing but actually does more. A function that looks simple might trigger logging, security checks, and transactions.
// This looks innocent
movePiece(game, "e2", "e4")
// But with aspects, it might actually:
// - Check permissions
// - Start a transaction
// - Log the call
// - Record changes
// - Send notifications
// - Commit the transaction
// - Log completionDebugging difficulty: When something goes wrong, the stack trace includes aspect code that doesn't appear in your source.
Aspect interactions: Multiple aspects might apply to the same code. Their order matters, but it's not always obvious.
Overuse: Once you have aspects, everything looks like a cross-cutting concern. Resist the temptation to aspect-ify everything.
When to Use Aspects
Use aspects for:
- Concerns that truly cut across many modules
- Infrastructure that shouldn't pollute business logic
- Behavior that needs to be uniformly applied
- Concerns that might need to be globally enabled/disabled
Avoid aspects for:
We've seen how aspects weave behavior throughout a codebase. Now let's put these tools together for a real challenge: keeping state synchronized across multiple clients. The next chapter explores distributed state---where proxies, diffing, and change tracking combine to enable collaboration.
- Logic that belongs in a specific module
- Behavior that's only needed in a few places
- Core business rules (these should be explicit)
- Anything where "magic" would confuse maintainers