Skip to main content
Low-Level Design · lesson 17 of 18
Open the Low-Level Design page →
Low-Level Design advanced Lesson 17 of 18

Also part of: Go

Design Patterns That Are Just Language Features: Strategy, Command, Iterator

Strategy becomes a function, Command a closure, Iterator a generator, run in Python, Java 21 and Go 1.23, with the cases where the class should stay.

By SkillByExample · Published

lld design-patterns strategy command iterator python java go

Several Gang of Four patterns describe what a 1994 C++ programmer had to build by hand: an object that holds one function, an object that remembers a call for later, an object that walks a collection. Languages with first-class functions, closures and generators give you those objects for free. This lesson runs each pattern the classic way and then the language-feature way, in Python, Java and Go. It also shows the cases where the class is still the better choice.

Strategy is a function

The classic Strategy is an interface with one method, plus one class per algorithm. Here are three discount rules at a checkout.

from abc import ABC, abstractmethod

class Discount(ABC):
    @abstractmethod
    def apply(self, total: float) -> float: ...

class NoDiscount(Discount):
    def apply(self, total): return total

class PercentOff(Discount):
    def __init__(self, pct): self.pct = pct
    def apply(self, total): return total * (1 - self.pct / 100)

class FlatOff(Discount):
    def __init__(self, amount): self.amount = amount
    def apply(self, total): return max(total - self.amount, 0)

def checkout(total, discount: Discount):
    return round(discount.apply(total), 2)

for d in [NoDiscount(), PercentOff(10), FlatOff(15)]:
    print(type(d).__name__, checkout(120.0, d))
$ python discount_classes.py
NoDiscount 120.0
PercentOff 108.0
FlatOff 105.0

Each class has a single method, and checkout only calls that one method. An interface with one method is a function type with extra ceremony. Python can say so directly:

from typing import Callable

Discount = Callable[[float], float]

def no_discount(total): return total

def percent_off(pct) -> Discount:
    return lambda total: total * (1 - pct / 100)

def flat_off(amount) -> Discount:
    return lambda total: max(total - amount, 0)

def checkout(total, discount: Discount):
    return round(discount(total), 2)

for name, d in [("none", no_discount), ("10%", percent_off(10)),
                ("15 off", flat_off(15)), ("adhoc", lambda t: t - 1)]:
    print(name, checkout(120.0, d))
$ python discount_functions.py
none 120.0
10% 108.0
15 off 105.0
adhoc 119.0

The first three totals match the class version. The constructor arguments (pct, amount) moved into closures, and the base class became a type alias. The fourth line shows what the class version could not do cheaply: a one-off rule written inline at the call site, with no new class.

In Java the interface stays, because lambdas need a target type. What disappears is the implementing classes. A lambda and a method reference satisfy the same interface that PercentOff implements:

import java.util.Map;

public class Discounts {
    static double memberPrice(double total) { return total * 0.8; }

    public static void main(String[] args) {
        Map<String, Discount> all = Map.of(
            "class", new PercentOff(10),
            "lambda", total -> Math.max(total - 15, 0),
            "method ref", Discounts::memberPrice);
        for (var name : new String[] {"class", "lambda", "method ref"}) {
            System.out.println(name + " -> " + all.get(name).apply(120.0));
        }
    }
}

@FunctionalInterface
interface Discount { double apply(double total); }

class PercentOff implements Discount {          // the classic version
    private final double pct;
    PercentOff(double pct) { this.pct = pct; }
    public double apply(double total) { return total * (1 - pct / 100); }
}
$ java Discounts.java
class -> 108.0
lambda -> 105.0
method ref -> 96.0

Discount has exactly one abstract method, so it is a functional interface. The @FunctionalInterface annotation only makes the compiler enforce that rule. memberPrice is an ordinary static method, and Discounts::memberPrice turns it into a strategy without changing it. public class Discounts comes first in the file because java File.java runs the first class it finds.

Go adds an idiom that lets you keep both forms. A named func type can have methods, so a plain function can satisfy the interface. The standard library uses the same trick for http.HandlerFunc:

package main

import "fmt"

type Discount interface{ Apply(total float64) float64 }

type PercentOff struct{ Pct float64 } // the classic version

func (p PercentOff) Apply(t float64) float64 { return t * (1 - p.Pct/100) }

// DiscountFunc lets a plain func satisfy Discount, like http.HandlerFunc.
type DiscountFunc func(float64) float64

func (f DiscountFunc) Apply(t float64) float64 { return f(t) }

func checkout(total float64, d Discount) float64 { return d.Apply(total) }

func main() {
	flat := DiscountFunc(func(t float64) float64 { return max(t-15, 0) })
	fmt.Println("struct:", checkout(120, PercentOff{Pct: 10}))
	fmt.Println("func:  ", checkout(120, flat))
}
$ go run discounts.go
struct: 108
func:   105

checkout accepts a Discount interface, and both a struct and a converted function satisfy it. A library can therefore expose the interface for callers that need a full type, and callers with a one-line rule can pass a function. (max on floats is a builtin since Go 1.21.)

When Strategy should stay a class

The function version wins while a strategy is one stateless operation. It loses in three situations, and each one shows up in real output.

It needs a name when it fails. The same bad config value, the string "10", goes through three Python strategies:

import traceback

class PercentOff:
    def __init__(self, pct): self.pct = pct
    def apply(self, total): return total * (1 - self.pct / 100)

def percent_off_lambda(pct):
    return lambda total: total * (1 - pct / 100)

def percent_off_def(pct):
    def percent_off(total): return total * (1 - pct / 100)
    return percent_off

bad = "10"  # a percentage read from config, still a string
for strategy in [PercentOff(bad).apply, percent_off_lambda(bad), percent_off_def(bad)]:
    try:
        strategy(120.0)
    except TypeError as e:
        frame = traceback.extract_tb(e.__traceback__)[-1]
        print(f"line {frame.lineno}, in {frame.name}")
$ python frame_names.py
line 5, in apply
line 8, in <lambda>
line 11, in percent_off

Every lambda in a Python program is named <lambda>, so in a log full of tracebacks only the line number identifies it. A nested def costs one extra line and gets a real name, which is usually all a Python strategy needs. Java’s names are less helpful:

public class StackNames {
    public static void main(String[] args) {
        Discount flat = total -> {
            if (total < 0) throw new IllegalArgumentException("negative total");
            return total - 15;
        };
        for (Discount d : new Discount[] {new PercentOff(10), flat}) {
            try {
                d.apply(-1);
            } catch (IllegalArgumentException e) {
                System.out.println(e.getStackTrace()[0]);
            }
        }
    }
}

interface Discount { double apply(double total); }

class PercentOff implements Discount {
    private final double pct;
    PercentOff(double pct) { this.pct = pct; }
    public double apply(double total) {
        if (total < 0) throw new IllegalArgumentException("negative total");
        return total * (1 - pct / 100);
    }
}
$ java StackNames.java
PercentOff.apply(StackNames.java:23)
StackNames.lambda$main$0(StackNames.java:4)

The class shows up as PercentOff.apply. The lambda shows up as lambda$main$0, a name javac generates from the enclosing method and a counter. It says nothing about discounts, and it can change when someone adds another lambda to main. If on-call engineers will read this strategy’s stack traces, give it a class name.

It carries state and has more than one method. Take a promotion that halves the first two orders and can also report how many uses are left:

class FirstNOrders:
    """Half price for the first n orders, then full price."""
    def __init__(self, n): self.left = n
    def apply(self, total):
        if self.left == 0:
            return total
        self.left -= 1
        return total / 2
    def describe(self): return f"{self.left} half-price uses left"

def first_n_orders(n):
    left = n
    def apply(total):
        nonlocal left
        if left == 0:
            return total
        left -= 1
        return total / 2
    def describe(): return f"{left} half-price uses left"
    return apply, describe          # two functions sharing one hidden variable

promo = FirstNOrders(2)
apply, describe = first_n_orders(2)
for _ in range(3):
    print(promo.apply(100.0), promo.describe(), "|", apply(100.0), describe())
$ python stateful_promo.py
50.0 1 half-price uses left | 50.0 1 half-price uses left
50.0 0 half-price uses left | 50.0 0 half-price uses left
100.0 0 half-price uses left | 100.0 0 half-price uses left

The two columns behave the same, so closures can carry state. Look at what the closure version returns, though: two functions that share a hidden left, which every caller has to unpack and pass around together. That is an object with its name and its self removed. Once a strategy has state and a second method, the class is the more honest shape.

You need to compare it. Go makes this one a compile error:

package main

import "fmt"

type PercentOff struct{ Pct float64 }

func main() {
	a, b := PercentOff{10}, PercentOff{10}
	fmt.Println("structs equal:", a == b)

	half := func(t float64) float64 { return t / 2 }
	current := half
	fmt.Println("same func:", current == half)
}
$ go run compare.go
# command-line-arguments
./compare.go:13:28: invalid operation: current == half (func can only be compared to nil)

Column 28 of line 13 is current, because the leading tab counts as one column. Go func values cannot be compared, so code that needs to ask “is the active discount the 10% one?”, remove a strategy from a list, or use one as a map key cannot do it with funcs. Two PercentOff structs with the same field compare equal. Python and Java accept the comparison but only check identity: in Python, percent_off(10) == percent_off(10) is False.

Command is a closure

A classic Command is an object that stores a receiver and its arguments, so that a call can run later, be queued, or be undone. That is what a closure does: it captures variables from the scope where it was created. The Python design patterns lesson already builds class-based commands for a text editor, so this Python version starts with closures. Each operation pushes its own inverse onto an undo stack:

class Account:
    def __init__(self): self.balance = 0
    def deposit(self, amount): self.balance += amount
    def withdraw(self, amount): self.balance -= amount

def main():
    acct, undo_stack = Account(), []

    def run(action, undo):
        action()
        undo_stack.append(undo)

    run(lambda: acct.deposit(100), lambda: acct.withdraw(100))
    run(lambda: acct.withdraw(30), lambda: acct.deposit(30))
    run(lambda: acct.deposit(5), lambda: acct.withdraw(5))
    print("balance:", acct.balance)
    while undo_stack:
        undo_stack.pop()()
        print("undo ->", acct.balance)

main()
$ python undo_closures.py
balance: 75
undo -> 70
undo -> 100
undo -> 0

Each lambda captures acct from main’s scope, plus a literal amount. That is the receiver and arguments a Deposit command class would store in __init__. The undo stack is a list of zero-argument functions, and undo_stack.pop()() pops one and calls it. The undos run in reverse: first the +5 is removed, then the -30 is restored, then the +100 is removed.

Java shows both forms in one file. Since Java 16 a record makes the classic command almost as short as the lambda. Runnable is the JDK’s built-in “no arguments, no result” function type:

import java.util.ArrayDeque;
import java.util.Deque;

public class BankUndo {
    static class Account { int balance; }

    interface Command { void execute(); void undo(); }       // the classic version

    record Deposit(Account acct, int amount) implements Command {
        public void execute() { acct.balance += amount; }
        public void undo() { acct.balance -= amount; }
    }

    public static void main(String[] args) {
        Account acct = new Account();
        Command classic = new Deposit(acct, 100);
        classic.execute();

        Deque<Runnable> undo = new ArrayDeque<>();            // closures instead
        acct.balance -= 30;
        undo.push(() -> acct.balance += 30);
        acct.balance += 5;
        undo.push(() -> acct.balance -= 5);
        System.out.println("balance: " + acct.balance);
        while (!undo.isEmpty()) {
            undo.pop().run();
            System.out.println("undo -> " + acct.balance);
        }
        classic.undo();
        System.out.println("classic undo -> " + acct.balance);
    }
}
$ java BankUndo.java
balance: 75
undo -> 70
undo -> 100
classic undo -> 0

The Deposit record needs a two-method interface because its undo is a separate operation from its execute. The closure version only stores the undo, since the action already ran inline. The lambdas can capture acct because that variable is never reassigned. The Account object is mutated, but the reference to it is effectively final, which is all Java requires.

When Command should stay a class

The loop-variable trap. Here are the same deposits pushed from a loop:

class Account:
    def __init__(self): self.balance = 0
    def deposit(self, amount): self.balance += amount
    def withdraw(self, amount): self.balance -= amount

def main():
    acct, undo_stack = Account(), []
    for amount in [100, 30, 5]:
        acct.deposit(amount)
        undo_stack.append(lambda: acct.withdraw(amount))
    print("balance:", acct.balance)
    print("captured:", undo_stack[0].__code__.co_freevars)

    while undo_stack:
        undo_stack.pop()()
    print("after undoing everything:", acct.balance)

main()
$ python late_binding.py
balance: 135
captured: ('acct', 'amount')
after undoing everything: 120

Undoing all three deposits should reach 0, but the program reaches 120. co_freevars lists the names the lambda closes over. A Python closure captures the variable amount, not its value at that moment. By the time any undo runs, the loop has finished and amount is 5, so all three undos withdraw 5 (135 − 15 = 120). A Withdraw(acct, amount) object would have copied the value in its constructor. Java refuses to compile the same shape:

import java.util.ArrayDeque;
import java.util.Deque;

public class UndoLoop {
    static class Account { int balance; }

    public static void main(String[] args) {
        Account acct = new Account();
        int[] amounts = {100, 30, 5};
        Deque<Runnable> undo = new ArrayDeque<>();
        for (int i = 0; i < amounts.length; i++) {
            acct.balance += amounts[i];
            undo.push(() -> acct.balance -= amounts[i]);
        }
        while (!undo.isEmpty()) undo.pop().run();
        System.out.println("after undoing everything: " + acct.balance);
    }
}
$ java UndoLoop.java
UndoLoop.java:13: error: local variables referenced from a lambda expression must be final or effectively final
            undo.push(() -> acct.balance -= amounts[i]);
                                                    ^
1 error
error: compilation failed

Line 13 is the undo.push line, and the caret points at i. The loop’s i++ reassigns i, so Java will not let a lambda capture it. This rule is why Java has no late-binding bug: a lambda can only capture a variable that never changes, so capturing the variable and capturing its value are the same thing. Go gives a third answer:

package main

import "fmt"

type Account struct{ Balance int }

func main() {
	acct := &Account{}
	var undo []func()
	for _, amount := range []int{100, 30, 5} {
		acct.Balance += amount
		undo = append(undo, func() { acct.Balance -= amount })
	}
	fmt.Println("balance:", acct.Balance)

	for len(undo) > 0 {
		last := undo[len(undo)-1]
		undo = undo[:len(undo)-1]
		last()
	}
	fmt.Println("after undoing everything:", acct.Balance)
}
$ go run undo_loop.go
balance: 135
after undoing everything: 0

Since Go 1.22, each loop iteration gets a fresh amount, so every closure keeps its own value. Before 1.22 Go had Python’s bug. You can see it by copying the file to undo_loop_go121.go and adding //go:build go1.21 plus a blank line above package main. That build constraint sets the file’s language version back to 1.21:

$ go run undo_loop_go121.go
balance: 135
after undoing everything: 120

The output is 120, Python’s wrong answer. So a closure command is only as safe as the language’s capture rule, and that rule changed in a Go minor release. The same goes for a module whose go.mod still says go 1.21.

It has to leave the process. Commands are often queued for a worker, written to a log, or replayed later. All of these need the command as data:

import pickle
from dataclasses import dataclass

@dataclass
class Deposit:
    account_id: str
    amount: int

def deposit_closure(account_id, amount):
    return lambda ledger: ledger.update({account_id: ledger[account_id] + amount})

for cmd in [Deposit("alice", 100), deposit_closure("alice", 100)]:
    try:
        payload = pickle.dumps(cmd)          # e.g. to put it on a job queue
        print("queued:", pickle.loads(payload))
    except Exception as e:
        print(f"{type(e).__name__}: {e}")
$ python queue_commands.py
queued: Deposit(account_id='alice', amount=100)
AttributeError: Can't get local object 'deposit_closure.<locals>.<lambda>'

The dataclass pickles, comes back intact, and prints a readable repr you could put in an audit log. The closure fails, because pickle stores functions by importable name and a lambda has none. A lambda at module level fails too, with PicklingError. Java and Go hit the same wall: a lambda or a func value has no data you can serialize. If a command must be stored, sent over the network, or displayed in an audit trail, make it a data type.

Iterator is a generator

An Iterator walks a structure one element at a time and hides how the structure is laid out. The interesting case is a structure that isn’t a flat list. Walking a binary search tree in order (left subtree, node, right subtree) as a hand-written iterator means managing your own stack:

class Node:
    def __init__(self, value, left=None, right=None):
        self.value, self.left, self.right = value, left, right

class InOrder:
    def __init__(self, root):
        self.stack = []
        self._push_left(root)
    def _push_left(self, node):
        while node:
            self.stack.append(node)
            node = node.left
    def __iter__(self):
        return self
    def __next__(self):
        if not self.stack:
            raise StopIteration
        node = self.stack.pop()
        self._push_left(node.right)
        return node.value

tree = Node(4, Node(2, Node(1), Node(3)), Node(6, Node(5)))
print(list(InOrder(tree)))
$ python tree_iter_class.py
[1, 2, 3, 4, 5, 6]

The traversal is correct but hard to read. The algorithm “left, self, right” is split between _push_left and __next__, and the stack does the job the call stack would do in a recursive function. A generator lets you write the recursive function and still return one value at a time:

class Node:
    def __init__(self, value, left=None, right=None):
        self.value, self.left, self.right = value, left, right

def in_order(node):
    if node:
        yield from in_order(node.left)
        yield node.value
        yield from in_order(node.right)

tree = Node(4, Node(2, Node(1), Node(3)), Node(6, Node(5)))
print(list(in_order(tree)))

values = in_order(tree)
print(next(values), next(values))
print(list(values))
print(list(values))
$ python tree_generator.py
[1, 2, 3, 4, 5, 6]
1 2
[3, 4, 5, 6]
[]

The four-line body says “left, self, right” directly. yield suspends the function and keeps its local variables and position, which the class had to store in self.stack by hand. The last two lines show the catch that applies to the class as well: an iterator is single-pass. After two next calls and one list, the generator is exhausted, so the second list returns [] with no error. To walk the tree again, call in_order(tree) again.

When the generator needs care

Recursion has a depth limit. A tree built from sorted input with no rebalancing is one long left spine. Here is a 5000-node one, walked by a recursive generator and by a generator that keeps the explicit stack:

class Node:
    def __init__(self, value, left=None, right=None):
        self.value, self.left, self.right = value, left, right

def in_order_recursive(node):
    if node:
        yield from in_order_recursive(node.left)
        yield node.value
        yield from in_order_recursive(node.right)

def in_order_stack(node):
    stack = []
    while stack or node:
        while node:
            stack.append(node)
            node = node.left
        node = stack.pop()
        yield node.value
        node = node.right

root = None
for value in range(5000, 0, -1):     # a "tree" that is one long left spine
    root = Node(value, left=root)

for walk in [in_order_stack, in_order_recursive]:
    try:
        print(walk.__name__, sum(1 for _ in walk(root)), "values")
    except RecursionError as e:
        print(walk.__name__, "RecursionError:", e)
$ python deep_tree.py
in_order_stack 5000 values
in_order_recursive RecursionError: maximum recursion depth exceeded

Each nested yield from is a live frame, and CPython’s default limit is 1000. The fix keeps the generator: in_order_stack has the same explicit stack as the InOrder class but none of the __iter__/__next__/StopIteration code. In Python, the generator replaces the iterator class in both cases. Recursion is a separate choice that a deep structure can rule out.

Java has no yield for iterators. Java’s yield keyword only returns a value from a switch expression. With no generators, the hand-written iterator is still the standard way to walk a tree lazily. A lambda can at least supply the Iterable so that for-each works:

import java.util.*;

public class TreeIterator {
    record Node(int value, Node left, Node right) {}

    static class InOrder implements Iterator<Integer> {
        private final Deque<Node> stack = new ArrayDeque<>();
        InOrder(Node root) { pushLeft(root); }
        private void pushLeft(Node n) { for (; n != null; n = n.left()) stack.push(n); }
        public boolean hasNext() { return !stack.isEmpty(); }
        public Integer next() {
            Node n = stack.pop();
            pushLeft(n.right());
            return n.value();
        }
    }

    public static void main(String[] args) {
        Node tree = new Node(4, new Node(2, new Node(1, null, null), new Node(3, null, null)),
                                new Node(6, new Node(5, null, null), null));
        Iterable<Integer> values = () -> new InOrder(tree);
        List<Integer> out = new ArrayList<>();
        for (int v : values) out.add(v);           // for-each works on any Iterable
        System.out.println(out);
    }
}
$ java TreeIterator.java
[1, 2, 3, 4, 5, 6]

Iterable has one abstract method, iterator(), so a lambda can implement it. That is the Strategy lesson again, and here it is the only part of the pattern that shrinks. A Stream looks like the generator-shaped answer, since you can write the recursion directly. Count the nodes it touches to get just the first value:

import java.util.stream.Stream;

public class TreeStream {
    record Node(int value, Node left, Node right) {}

    static int visited = 0;

    static Stream<Integer> inOrder(Node n) {
        if (n == null) return Stream.empty();
        visited++;
        return Stream.concat(Stream.concat(inOrder(n.left()), Stream.of(n.value())),
                             inOrder(n.right()));
    }

    public static void main(String[] args) {
        Node tree = new Node(4, new Node(2, new Node(1, null, null), new Node(3, null, null)),
                                new Node(6, new Node(5, null, null), null));
        System.out.println(inOrder(tree).toList());

        visited = 0;
        System.out.println("first: " + inOrder(tree).findFirst().get()
                           + ", nodes visited: " + visited);
    }
}
$ java TreeStream.java
[1, 2, 3, 4, 5, 6]
first: 1, nodes visited: 6

The result is right, but findFirst touched all 6 nodes. The InOrder iterator only pushes the left spine (4, 2, 1) before returning 1. The elements flow lazily, but the recursion calls inOrder on every node to build the nested concat pipeline before the first element is pulled. The Stream.concat Javadoc also warns that deeply nested concatenation can throw StackOverflowError. In Java the iterator class survives this lesson.

Go 1.23 has iterator functions. An iter.Seq[int] is a function that takes a yield callback, and for range accepts it directly. Go turns the loop body into the yield function:

package main

import (
	"fmt"
	"iter"
)

type Node struct {
	Value       int
	Left, Right *Node
}

func (n *Node) InOrder() iter.Seq[int] {
	return func(yield func(int) bool) { n.walk(yield) }
}

func (n *Node) walk(yield func(int) bool) bool {
	if n == nil {
		return true
	}
	return n.Left.walk(yield) && yield(n.Value) && n.Right.walk(yield)
}

func main() {
	leaf := func(v int) *Node { return &Node{Value: v} }
	tree := &Node{4, &Node{2, leaf(1), leaf(3)}, &Node{6, leaf(5), nil}}
	var got []int
	for v := range tree.InOrder() {
		if v > 4 {
			break
		}
		got = append(got, v)
	}
	fmt.Println(got)
}
$ go run tree_seq.go
[1 2 3 4]

walk is the recursive in-order traversal, and yield returns false when the loop body breaks. The && chain stops walking as soon as any call returns false, so 6 is never yielded. Unlike a Python generator, this iter.Seq is not consumed: ranging over tree.InOrder() again starts a new walk. Some iterators are single-use, though, such as one that reads from a channel or a file. Go’s version also has a sharp edge: the iterator function must respect that false. This one ignores it:

package main

import "fmt"

type Node struct {
	Value       int
	Left, Right *Node
}

func (n *Node) walk(yield func(int) bool) { // ignores what yield returns
	if n == nil {
		return
	}
	n.Left.walk(yield)
	yield(n.Value)
	n.Right.walk(yield)
}

func main() {
	leaf := func(v int) *Node { return &Node{Value: v} }
	tree := &Node{4, &Node{2, leaf(1), leaf(3)}, &Node{6, leaf(5), nil}}
	for v := range tree.walk {
		if v > 2 {
			break
		}
		fmt.Println(v)
	}
}
$ go run tree_seq_bad.go
1
2
panic: runtime error: range function continued iteration after function for loop body returned false

goroutine 1 [running]:
main.main-range1(0xc0000a4ec8?)
	/src/tree_seq_bad.go:22 +0xa5
main.(*Node).walk(0xc0000a4ee0, 0xc0000a4ea0)
	/src/tree_seq_bad.go:15 +0x3b
main.main()
	/src/tree_seq_bad.go:22 +0x107
exit status 2

The loop body hit break at 3, so yield returned false. walk called yield again anyway for 4, and the runtime panicked instead of silently running a loop body that had already exited. Line 15 is the yield(n.Value) call that ignored the result. Line 22 is the for range loop, whose body Go compiled into the function main-range1. The hex addresses differ from run to run. A method value with the right signature (tree.walk) can be ranged over directly with no iter.Seq wrapper. A Python generator can’t make this mistake: it pauses at yield until the consumer asks for more. A Go iterator function stays in control of its own loop, so the stopping protocol is the author’s job.

The rule of thumb

PatternUse the language feature when…Keep the class when…
Strategyit’s one stateless operationit holds state, has several methods, must be compared, or needs a readable name in traces
Commandit runs or undoes within this processit must be serialized, logged, replayed or inspected, or it captures loop variables in a language without per-iteration capture
Iteratorthe language has generators (Python) or iterator functions (Go 1.23+)the language has neither (Java), or the consumer needs more than “next value”, such as peek, remove or the remaining stack

The patterns themselves did not go away. What changed is how much code each one costs. The class version is still the right choice when the extra code buys a name, a type, or data you can store. For how a pattern grows out of a refactor rather than a catalog, see the Observer lesson earlier in this track.

Practice

1. Fix late_binding.py so that undoing everything returns the balance to 0, using functools.partial.
from functools import partial

class Account:
    def __init__(self): self.balance = 0
    def deposit(self, amount): self.balance += amount
    def withdraw(self, amount): self.balance -= amount

def main():
    acct, undo_stack = Account(), []
    for amount in [100, 30, 5]:
        acct.deposit(amount)
        undo_stack.append(partial(acct.withdraw, amount))
    print("balance:", acct.balance)
    print("queued undos:", [u.args for u in undo_stack])

    while undo_stack:
        undo_stack.pop()()
    print("after undoing everything:", acct.balance)

main()
$ python late_binding_fixed.py
balance: 135
queued undos: [(100,), (30,), (5,)]
after undoing everything: 0

partial evaluates amount when it is created, so each undo holds the value from its own iteration. It also fixes part of the inspection problem: a partial exposes .func and .args, so you can print what is queued. You can’t do that with a lambda. The other common fix, lambda amount=amount: acct.withdraw(amount), works for the same reason: default arguments are evaluated once, when the function is defined.

2. Make UndoLoop.java compile without adding a final local copy.
import java.util.ArrayDeque;
import java.util.Deque;

public class UndoLoopFixed {
    static class Account { int balance; }

    public static void main(String[] args) {
        Account acct = new Account();
        int[] amounts = {100, 30, 5};
        Deque<Runnable> undo = new ArrayDeque<>();
        for (int amount : amounts) {
            acct.balance += amount;
            undo.push(() -> acct.balance -= amount);
        }
        System.out.println("balance: " + acct.balance);
        while (!undo.isEmpty()) undo.pop().run();
        System.out.println("after undoing everything: " + acct.balance);
    }
}
$ java UndoLoopFixed.java
balance: 135
after undoing everything: 0

An enhanced for declares a new amount variable on every iteration, and nothing reassigns it. That makes it effectively final, so the lambda may capture it, and each capture sees its own value. This is the same per-iteration rule Go adopted in 1.22, though Java only applies it to the enhanced for. The index variable i in a classic for is still reassigned by i++.

3. Keep the stateful FirstNOrders promotion a class, but let it go anywhere a function-typed Discount is expected.
from typing import Callable

Discount = Callable[[float], float]

def checkout(total, discount: Discount):
    return round(discount(total), 2)

class FirstNOrders:
    """Half price for the first n orders, then full price."""
    def __init__(self, n): self.left = n
    def __call__(self, total):
        if self.left == 0:
            return total
        self.left -= 1
        return total / 2
    def describe(self): return f"{self.left} half-price uses left"

promo = FirstNOrders(2)
for d in [lambda t: t - 10, promo, promo, promo]:
    print(checkout(100.0, d), "|", promo.describe())
$ python callable_promo.py
90.0 | 2 half-price uses left
50.0 | 1 half-price uses left
50.0 | 0 half-price uses left
100.0 | 0 half-price uses left

Renaming apply to __call__ makes instances callable, so checkout cannot tell the promotion from a lambda. Both satisfy Callable[[float], float]. You keep the class’s name, state and second method, and simple strategies stay one-line lambdas. This is the Python counterpart of Go’s DiscountFunc adapter, working in the opposite direction.

4. In Go, write a generic Filter that takes an iter.Seq and a predicate and returns a new iter.Seq.
package main

import (
	"fmt"
	"iter"
	"slices"
)

// Filter combines both patterns: keep is a strategy, the result is an iterator.
func Filter[T any](seq iter.Seq[T], keep func(T) bool) iter.Seq[T] {
	return func(yield func(T) bool) {
		for v := range seq {
			if keep(v) && !yield(v) {
				return
			}
		}
	}
}

func main() {
	nums := slices.Values([]int{1, 2, 3, 4, 5, 6})
	even := func(n int) bool { return n%2 == 0 }
	fmt.Println(slices.Collect(Filter(nums, even)))
	fmt.Println(slices.Collect(Filter(nums, func(n int) bool { return n > 4 })))
}
$ go run filter_seq.go
[2 4 6]
[5 6]

keep is a Strategy passed as a function, and the returned iter.Seq is an Iterator built from a closure. !yield(v) followed by return is the stopping protocol that tree_seq_bad.go skipped. nums is used twice and works both times because slices.Values returns a function that starts a new walk on every call, which a Python generator does not do.

Frequently Asked Questions

Are design patterns obsolete in languages with first-class functions?
No. The problems they solve remain, and some of the code for them disappears. A Strategy with one stateless method becomes a function, and a Command that only needs to run or undo in-process becomes a closure. Both still need a class once they carry state, have several methods, or must be named, compared or serialized.
What is the difference between the Command pattern and a closure?
Both capture a receiver and arguments so a call can run later. A Command object stores them as named fields, so it can be printed, compared, serialized to a queue or log, and given extra methods such as undo. A closure stores them invisibly. That is shorter, but in Python, for example, pickle cannot serialize a lambda or a nested function at all.
Why do closures created in a loop all see the last value?
Python closures capture the variable, not its value at creation time, so every closure built in a loop reads the variable's final value. Fix it with functools.partial or a default argument such as lambda amount=amount: .... Java avoids the bug by only letting lambdas capture effectively final variables, and Go 1.22 gave each loop iteration its own variable.
Does Java have generators like Python's yield?
No. Java's yield keyword only returns a value from a switch expression. To walk a recursive structure lazily, Java code still writes an Iterator class with an explicit stack. A recursive Stream.concat looks shorter but builds the whole pipeline first, visiting every node even for findFirst, and its Javadoc warns that deep concatenation can overflow the stack.
How do Go 1.23 iterators work with for range?
An iter.Seq[T] is a function that takes a yield callback, and for range calls it with the loop body compiled into yield. yield returns false when the body breaks, and the iterator must then stop. If it calls yield again, the runtime panics with "range function continued iteration after function for loop body returned false".