Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25, Go 1.27.1, Clang 21.0.0 and Rust 1.99.0
  • By MySmartCoPilot
Free intro, Pro: 8 of 30 minutes free
Free: A Strategy as a function. With a pass: the other 8 sections and 3 interview answers, about 22 minutes.

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

Strategy with classes needs an interface and a class per rule; with functions, the checkout holds the rule function itself.With classes: 3 typesWith functions: no new typesCheckoutholds a ShippingRuleShippingRulefee(order)FlatFreeAbove(threshold)Checkoutholds a functionflat(order)partial(free_above, 49900)callsimplemented bycalls one ofsame behaviour

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.

  1. 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).
  2. 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:

Two shipping rules as classes and as functions Python · strategy/shipping.py
"""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

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.

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.