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:

javascript
    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:

javascript
    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:

erlang
    // 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 captured

The 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.

erlang
    // 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.

javascript
    aspect Security:
        before any function matching "move*":
            if not hasPermission(currentUser, "move"):
                throw "Not authorized"

After advice: Runs after the original code.

erlang
    aspect Notification:
        after any function matching "*Piece":
            notifyObservers("piece changed")

Around advice: Wraps the original code, controlling whether it runs.

wollok
    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 result

Change Tracking as an Aspect

Remember our change tracking from earlier chapters? It's a perfect fit for aspects:

erlang
    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 result

Now 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.

python
    // 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")       // injected

At runtime: A framework intercepts calls dynamically.

python
    // Proxy-based weaving
    movePiece = createProxy(originalMovePiece, {
        apply(target, thisArg, args):
            runBeforeAdvice()
            result = target.apply(thisArg, args)
            runAfterAdvice()
            return result
    })

Real-World Aspects

Transaction Management

javascript
    aspect Transactions:
        around any function annotated with @Transactional:
            beginTransaction()
            try:
                result = proceed()
                commitTransaction()
                return result
            catch error:
                rollbackTransaction()
                throw error

Performance 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 result

Retry Logic

javascript
    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.

python
    // 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 completion

Debugging 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