Patterns of Collaboration

A closure is a strange thing. It's a function that remembers. When we create a counter that increments each time it's called, where does the count live? Inside the function, invisible from outside, changing with each call.

This is an object in disguise.

From Closures to Objects: The Bridge

Let's make this concrete. Here's a counter as a closure:

    make_counter():
        count = 0
        return () =>
            count = count + 1
            return count
    
    counter = make_counter()
    counter()    // 1
    counter()    // 2
    counter()    // 3

Now the same thing as an object:

wollok
    class Counter {
        var count = 0    // private by default in Wollok
        
        method increment() {
            count = count + 1
            return count
        }
    }
    
    const counter = new Counter()
    counter.increment()    // 1
    counter.increment()    // 2
    counter.increment()    // 3

The parallel is exact:

  • State: The closure's captured variables = the object's private fields
  • Behavior: Calling the closure = sending a message to the object
  • Encapsulation: The captured scope is hidden = private fields are hidden
A closure and an object: both bundle state with behavior.

In dynamic languages like JavaScript, you can construct objects from closures directly:

    make_counter():
        count = 0
        return {
            increment: () => { count = count + 1; return count },
            reset: () => { count = 0 },
            value: () => count
        }

This bridge between paradigms reveals something deep: closures and objects are two syntaxes for the same idea—encapsulated state with controlled access.

Methods as Functions: The Receiver is Just a Parameter

Here's another lens shift. Consider a method call:

    piece.canMoveTo(square)

In OOP, we think: "ask the piece if it can move to that square." The piece is special—it's the receiver of the message.

But we can also see it as:

    canMoveTo(piece, square)

A function with parameters. The "receiver" is just the first argument. Nothing magical.

This view unifies paradigms:

  • OOP view: Objects receive messages; behavior belongs to the object
  • FP view: Functions take data; behavior is separate from data
  • Unified view: Both are function calls; the question is where behavior is organized

Understanding both views makes you fluent in translating between paradigms—and choosing the right one for each situation.

Discovering Patterns Through Problems

Design patterns aren't inventions—they're discoveries. Programmers solving similar problems independently arrive at similar solutions. The patterns are named so we can discuss them.

Let's discover some patterns by solving problems.

The Problem: Different Ways to Process a Collection

We have a list of chess moves. Different contexts need different things:

  • Display: show all moves
  • Analysis: show only captures
  • Scoring: transform moves to their values, then sum

The Iterator: Walking Through Elements

First, we need a way to walk through elements one at a time:

wollok
    class Iterator {
        method hasNext()    // returns Boolean
        method next()       // returns Element
    }
    
    // Usage in Wollok
    while (iterator.hasNext()) {
        const element = iterator.next()
        // do something with element
    }

An iterator over a list:

wollok
    class ListIterator {
        const list
        var position = 0
        
        method hasNext() = position < list.size()
        
        method next() {
            const element = list.get(position)
            position = position + 1
            return element
        }
    }

This is the Iterator pattern: an object that knows how to traverse a collection, exposing elements one at a time.

The Decorator: Wrapping to Add Behavior

Now suppose we want an iterator that only yields captures. We could write a new iterator from scratch. Or...

wollok
    class FilteringIterator {
        const source      // another iterator
        const predicate   // a block/closure
        
        method hasNext() {
            self.advanceToNextMatch()
            return source.hasNext()
        }
        
        method next() = source.next()
        
        method advanceToNextMatch() {
            while (source.hasNext()) {
                if (predicate.apply(source.peek())) return
                source.next()  // skip non-matching
            }
        }
    }
wollok
    // Compose iterators (Wollok)
    const allMoves = new ListIterator(list = moves)
    const onlyCaptures = new FilteringIterator(
        source = allMoves, 
        predicate = { m => m.isCapture() }
    )
    // onlyCaptures yields only capture moves

This is the Decorator pattern: wrapping an object to add or modify behavior while keeping the same interface. The wrapper decorates the original.

Decorator: the FilteringIterator wraps a ListIterator, same interface.

The Strategy: Pluggable Algorithms

Our filtering iterator takes a predicate—a function that decides what passes through. We can swap predicates without changing the iterator:

wollok
    // Different strategies, same iterator structure (Wollok)
    const captures = new FilteringIterator(source=moves, predicate={ m => m.isCapture() })
    const checks = new FilteringIterator(source=moves, predicate={ m => m.givesCheck() })
    const byQueen = new FilteringIterator(source=moves, predicate={ m => m.piece().kind() == queen })

The predicate is a Strategy: an algorithm encapsulated so it can be swapped. The iterator doesn't know which predicate it has—it just uses whatever was provided.

This is the Strategy pattern: define a family of algorithms, encapsulate each one, and make them interchangeable.

The Composite: Things Containing Things of the Same Kind

Now a different problem. In chess, most moves are simple: one piece moves from one square to another. But castling involves two pieces moving simultaneously—the king and the rook.

How do we model this?

sql
    Move = 
        | SimpleMove(piece, from, to)
        | ???  // What about castling?

One option: a composite move that contains other moves.

wollok
    // Wollok: using polymorphism instead of pattern matching
    class SimpleMove {
        const piece
        const from
        const to
        
        method execute(board) {
            board.clear(from)
            board.put(piece, to)
        }
    }
    
    class CompositeMove {
        const moves    // List of Move
        
        method execute(board) {
            moves.forEach { m => m.execute(board) }  // recursive!
        }
    }

Now castling is natural:

wollok
    const kingsideCastle = new CompositeMove(moves = [
        new SimpleMove(piece=king, from=e1, to=g1),
        new SimpleMove(piece=rook, from=h1, to=f1)
    ])

This is the Composite pattern: a component that can contain other components of the same type, forming a tree structure.

Composite: castling as a move containing moves.

The composite pattern appears everywhere:

  • File systems: folders contain files and folders
  • UI: containers hold widgets, which might be containers
  • Organizations: teams contain people and sub-teams
  • Expressions: (a + b) * c—operations containing operations

When Behavior Depends on Two Types

So far, polymorphism has depended on one type—the receiver of the message. "What does move do?" depends on what kind of piece is moving.

But sometimes behavior depends on multiple types.

The Collision Problem

Imagine a game where different objects collide:

python
    // What happens when X hits Y?
    Asteroid hits Asteroid  -> both shatter
    Asteroid hits Spaceship -> spaceship damaged
    Spaceship hits Asteroid -> asteroid shatters
    Laser hits Asteroid     -> asteroid shatters
    Laser hits Spaceship    -> spaceship damaged
    Laser hits Laser        -> nothing happens

The naive approach: a big conditional.

wollok
    // Naive approach - brittle conditionals
    method collide(a, b) {
        if (a.isAsteroid() && b.isAsteroid()) 
            shatterBoth(a, b)
        else if (a.isAsteroid() && b.isSpaceship()) 
            b.damage()
        else if (a.isSpaceship() && b.isAsteroid()) 
            b.shatter()
        // ... and on and on
    }

This is fragile. Adding a new type means updating every conditional. The logic is centralized and brittle.

Double Dispatch: Let Objects Negotiate

The insight: we can dispatch twice. First on one object, then on the other.

wollok
    // First dispatch: ask A to handle collision with B
    method collide(a, b) { a.collideWith(b) }
    
    // Each type delegates back - second dispatch (Wollok)
    class Asteroid {
        method collideWith(other) { other.hitByAsteroid(self) }
    }
    
    class Spaceship {
        method collideWith(other) { other.hitBySpaceship(self) }
    }
    
    class Laser {
        method collideWith(other) { other.hitByLaser(self) }
    }

Now each type implements the hit_by_X methods:

wollok
    class Asteroid {
        method hitByAsteroid(asteroid) { shatterBoth(self, asteroid) }
        method hitBySpaceship(ship) { self.shatter() }
        method hitByLaser(laser) { self.shatter() }
    }
    
    class Spaceship {
        method hitByAsteroid(asteroid) { self.damage() }
        method hitBySpaceship(ship) { /* nothing */ }
        method hitByLaser(laser) { self.damage() }
    }

This is double dispatch: using two polymorphic calls to select behavior based on two types. It's the core of the Visitor pattern.

The Visitor Pattern

Double dispatch generalizes to the Visitor pattern. The idea: separate an algorithm from the structure it operates on.

wollok
    // The structure: a chess position (Wollok)
    class Position {
        const pieces    // List of Piece
        
        method accept(visitor) {
            pieces.forEach { piece => piece.accept(visitor) }
        }
    }
    
    // Each piece accepts visitors
    class King {
        method accept(visitor) { visitor.visitKing(self) }
    }
    
    class Pawn {
        method accept(visitor) { visitor.visitPawn(self) }
    }
    // ... etc for each piece type

Now we can define different operations as visitors:

wollok
    // Count material value (Wollok)
    class MaterialCounter {
        var total = 0
        
        method visitKing(k)   { total += 0 }
        method visitQueen(q)  { total += 9 }
        method visitRook(r)   { total += 5 }
        method visitBishop(b) { total += 3 }
        method visitKnight(n) { total += 3 }
        method visitPawn(p)   { total += 1 }
    }

    // Render to display
    class Renderer {
        method visitKing(k)  { drawKing(k.position(), k.color()) }
        method visitQueen(q) { drawQueen(q.position(), q.color()) }
    }

The Visitor pattern separates:

  • Structure: The pieces and how to traverse them
  • Operations: What to do with each piece type

Adding new operations is easy—just create new visitors. Adding new types is harder—all visitors must be updated. This trade-off is fundamental.

Patterns Are Not Recipes

A caution: patterns are not recipes to follow blindly. They are:

  • Names for recurring solutions
  • Vocabulary for discussing design
  • Starting points for thinking, not endpoints

Every pattern has trade-offs:

  • Iterator: Hides collection structure, but adds objects
  • Decorator: Flexible composition, but many small classes
  • Strategy: Swappable algorithms, but indirection
  • Composite: Uniform treatment, but complex traversal
  • Visitor: Easy new operations, but hard new types

The question is never "which pattern should I use?" but "what problem am I solving, and what solution makes the code clearest?"

A Pattern Catalog

For reference, here are common patterns organized by intent:

Creational: How Objects Are Made

  • Factory: Delegate object creation to a method or class
  • Builder: Construct complex objects step by step
  • Singleton: Ensure only one instance exists

Structural: How Objects Compose

  • Decorator: Wrap to add behavior
  • Composite: Tree structures of uniform components
  • Proxy: Stand-in that controls access
  • Adapter: Convert one interface to another

Behavioral: How Objects Interact

  • Strategy: Pluggable algorithms
  • Iterator: Sequential access to elements
  • Observer: Notify dependents of changes
  • Visitor: Operations on structure elements
  • Command: Encapsulate a request as an object

We've traveled from closures to objects, seeing them as two expressions of the same idea. We've discovered patterns through problems: Iterator, Decorator, Strategy, Composite, Visitor. We've seen how double dispatch handles behavior that depends on multiple types.

These patterns aren't exclusive to OOP. The Strategy pattern is a function parameter. The Composite pattern is a recursive data type. The Visitor pattern is a fold over a structure. The paradigms blur when you look closely enough.

In the next chapter, we step back further—from specific patterns to the general art of abstraction itself.