Design Patterns (GoF and modern) Module 7 – Modern patterns for resources, runtime and composition
Functional patterns that replace classes
Replace Strategy, Command and Template Method with functions, configure behaviour with closures and partial application, and return errors as values.
What you will learn
- Replace Strategy, Command and Template Method with higher-order functions where that is clearer
- Compose functions and use partial application to configure behaviour
- Handle errors as values with Result, std::expected and union types
- Structure code as a functional core with an imperative shell
Before you start
What you’ll learn · Before you start
What you will learn
- Replace Strategy, Command and Template Method with higher-order functions where that is clearer
- Compose functions and use partial application to configure behaviour
- Handle errors as values with Result, std::expected and union types
- Structure code as a functional core with an imperative shell
Before you start
On this page
In a language where functions are values, several class-based patterns shrink to a function. A Strategy becomes a function passed as an argument, a Command becomes a function or a closure stored for later, and a Template Method becomes a function that takes its varying steps as parameters. Peter Norvig counted this in 1996: 16 of the 23 classic patterns had simpler forms in Lisp or Dylan, and first-class functions alone accounted for four of them, Command, Strategy, Template Method and Visitor. The same thinking gives three more tools: closures and partial application to configure behaviour, errors returned as values instead of thrown, and a design that keeps decisions in pure functions and input and output at the edges. Keep a class when the varying part holds state, has several operations or deserves a name in the domain.
A Strategy as a function
The same Strategy with classes and with functions
Text description of the diagram
The diagram shows the same two shipping rules twice, one design above the other.
- With classes, there are three types: Checkout holds a ShippingRule and calls its fee(order) method, and ShippingRule is implemented by two classes, Flat and FreeAbove(threshold).
- With functions, there are no new types: Checkout holds a function and calls one of two, flat(order) or partial(free_above, 49900), the general function with its threshold fixed.
An arrow between the two designs says that they behave the same: the function form keeps the Strategy's idea, a family of interchangeable rules chosen at run time, without the interface and the classes.
Each pattern has a functional form, and the idea behind it stays the same:
- Strategy: an interface and one class per algorithm become a function passed as an argument, or a table of functions.
- Command: an object with an
execute()method becomes a function or a closure, stored and called later. - Template Method: a base class whose steps subclasses override becomes a function that takes its varying steps as parameters.
Settings follow the same move. A class whose constructor stores a threshold or a percentage becomes a closure that captures it, or a general function with that argument fixed by partial application, so there is no class left to name, construct or keep in step with an interface.
The four ideas that the rest of this lesson builds on, each in one sentence:
- Higher-order functions take or return functions, which is all a Strategy or a Command needs.
- Partial application fixes some of a function’s arguments now and returns a function that takes the rest.
- Errors as values means returning a result that is either a value or an error, so the caller must look at it.
- Functional core, imperative shell keeps the decisions in pure functions and pushes input and output to a thin outer layer.
Here two shipping rules are written both ways. The class version needs an abstract base and a class per rule; the
function version is two functions, a dictionary of them, and functools.partial to fix the threshold of one:
"""Strategy without classes. The class version needs an interface and one class per rule; with first-class
functions, a rule is just a function, a table of rules is a dictionary, and functools.partial turns a general rule
into a configured one. Amounts are whole paise, so no rounding surprises."""
from abc import ABC, abstractmethod
from functools import partial
# ---- the class-based Strategy -------------------------------------------------------------------------------------
class ShippingRule(ABC):
@abstractmethod
def fee(self, order_paise: int) -> int: ...
class Flat(ShippingRule):
def fee(self, order_paise):
return 4_000
class FreeAbove(ShippingRule):
def __init__(self, threshold_paise):
self.threshold_paise = threshold_paise
def fee(self, order_paise):
return 0 if order_paise >= self.threshold_paise else 4_000
# ---- the same rules as functions ------------------------------------------------------------------------------------
def flat(order_paise):
return 4_000
def free_above(threshold_paise, order_paise):
return 0 if order_paise >= threshold_paise else 4_000
RULES = {"flat": flat, "free above 499": partial(free_above, 49_900)} # partial fixes the first argument
for order in (29_900, 49_900, 120_000):
by_class = [Flat().fee(order), FreeAbove(49_900).fee(order)]
by_function = [RULES["flat"](order), RULES["free above 499"](order)]
print(f"order {order} paise: classes {by_class}, functions {by_function}, same: {by_class == by_function}")
print("classes needed for two rules:", len(ShippingRule.__subclasses__()) + 1, "| functions needed:", 2) Output
order 29900 paise: classes [4000, 4000], functions [4000, 4000], same: True order 49900 paise: classes [4000, 0], functions [4000, 0], same: True order 120000 paise: classes [4000, 0], functions [4000, 0], same: True classes needed for two rules: 3 | functions needed: 2
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shipping.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Both versions agree on all three orders. The function version has no interface to keep in step and no class to name, and adding a rule is adding a function to the dictionary. What it gives up is the place to put more than one operation or any state of the rule’s own, which is exactly when the class form earns its keep.
A pricing engine: four classes, then four composed functions Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Closures and partial application: binding settings, and the loop trap
- Errors as values: a parser that reports every bad line
- Functional core, imperative shell
- When to keep the class, and the trap of anonymous chains
- Check yourself
- Practise: order validation as pure functions
- Order validation as pure functions that return results
- Interview questions
- How does the Strategy pattern look in a language with first-class functions?
- A teammate proposes replacing every Strategy and Command class in a payments service with lambdas. Which ones would you convert, which would you keep as classes, and how do you decide?
- A bulk-upload API rejects a 500-row file at the first invalid row, and customers resubmit it again and again. How would you change the validation to report every problem in one response, in Rust or TypeScript, and when are exceptions still the better tool?
- A subscription-renewal job reads the clock, queries the database, charges cards and sends emails inside one 400-line function, and its tests need eleven mocks. How would you restructure it as a functional core with an imperative shell, step by step?
- Key takeaways
1 diagram · 5 runnable examples (Engine.java, Python, engine.cpp, engine.go, engine.rs, engine.ts, engines_session.py, parse.cpp, parse.rs, parse.ts, results_session.py, session.py) · 1 exercise with sample tests · 3 interview answers · about 19 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- Design Patterns in Dynamic Languages (Peter Norvig, 1996) (Peter Norvig)
- functools: higher-order functions and operations on callable objects (Python Software Foundation)
- The Rust Programming Language: recoverable errors with Result (The Rust Project)
- libc++ C++23 status (P0323R12 std::expected) (LLVM Project)
- TypeScript handbook: narrowing and discriminated unions (Microsoft (TypeScript))
- Functional Core, Imperative Shell (screencast) (Destroy All Software)
- Package java.util.function (Java SE 25) (Oracle)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress