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 1 – Why patterns, and when not to use them

How language features reshape patterns

Why 16 of the 23 classic patterns get simpler with the right language features, and how Java, Python, TypeScript, Go, Rust and C++ express each one.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7 and Node.js 24.21.0
  • By MySmartCoPilot
Free: 30 minutes

What you will learn

  • Explain Norvig's finding that 16 of the 23 classic patterns become simpler or invisible in dynamic languages
  • Compare how Java, Python, TypeScript, Go, Rust and C++ express interfaces, polymorphism and composition
  • Choose between a class-based pattern and a function, closure, sum type or module equivalent
  • Read this track's multi-language examples and the toolchain versions they ran with

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain Norvig's finding that 16 of the 23 classic patterns become simpler or invisible in dynamic languages
  • Compare how Java, Python, TypeScript, Go, Rust and C++ express interfaces, polymorphism and composition
  • Choose between a class-based pattern and a function, closure, sum type or module equivalent
  • Read this track's multi-language examples and the toolchain versions they ran with

Before you start

On this page

A design pattern describes a shape of code, and how much code that shape needs depends on the language. A Strategy in classic Java is an interface and one class per algorithm; in Python, JavaScript or Go it is often just a function passed around; in Rust it is a generic parameter or a boxed closure. Peter Norvig’s study of the Gang of Four book, first presented in 1996, found that 16 of its 23 patterns have a simpler implementation in Lisp or Dylan than in C++, for at least some uses of each. The problem a pattern solves does not go away. What changes is how much structure you write to solve it, and sometimes the pattern disappears into a language feature you use without naming it.

This lesson shows one Strategy in four languages against a single table of test cases, maps the features of the six languages of this track that reshape patterns, and records the TypeScript syntax that Node.js refuses to run.

Patterns are written in a language

The Gang of Four wrote their sample code in C++ and Smalltalk and described every pattern in terms of classes, objects and inheritance. The C++ of the time had no closures and no way to treat a class as a value that a program can store and pass, and many of the patterns work around exactly that. Norvig grouped the 16 patterns that get simpler by the language feature that simplifies them:

Feature How many Patterns
First-class types 6 Abstract Factory, Flyweight, Factory Method, State, Proxy, Chain of Responsibility
First-class functions 4 Command, Strategy, Template Method, Visitor
Macros 2 Interpreter, Iterator
Method combination 2 Mediator, Observer
Multimethods 1 Builder
Modules 1 Facade

Two of those features matter most for the languages of this track. First-class functions, which every one of them now has in some form, shrink the patterns whose objects exist only to carry one piece of behaviour: Command, Strategy, Template Method and Visitor. First-class types, classes that are values you can store and pass, make many creational patterns a dictionary of classes, which Python and JavaScript do directly. The study’s own wording is careful: “for at least some uses of each pattern”. A Strategy with several operations and its own state still deserves a class.

One Strategy, four languages, one test table

A parking lot charges by one of three rules: 20 for every started hour, a flat 100, or the first hour free and then 30 for every started hour. The rule is chosen by name, and the lot must not change when a rule is added. That is Strategy, and the shape of its code depends on what the language gives you:

The same pricing Strategy drawn twice: an interface with three implementing classes, then three functions in a map.Class form: 1 interface + 3 classesFunction form: 3 functions in a mapParkingLot«interface»PricingRulefee(minutes)HourlyFlatFirstHourFreeParkingLotRULEShourly · flat ·first-hour-freeevery value:minutes → feeholds onelooks up one

One Strategy, two shapes: classes behind an interface, or functions in a map

Text description of the diagram

Two panels, one above the other.

The first panel, "Class form: 1 interface + 3 classes", shows a ParkingLot box with an arrow labelled "holds one" to an interface box, PricingRule, whose one operation is fee(minutes). Three classes, Hourly, Flat and FirstHourFree, each join the interface with a dashed line and a hollow triangle: each one realises it.

The second panel, "Function form: 3 functions in a map", shows a ParkingLot box with an arrow labelled "looks up one" to a map named RULES, holding hourly, flat and first-hour-free. A dashed arrow from the map leads to a note: every value is a function from minutes to a fee. The signature is the whole interface, and no class is needed.

In Python each rule is a plain function, and a dictionary picks one. The program reads the shared table of cases, cases.txt, and checks every fee against it:

Strategy as functions in Python

Python · strategy/strategy.py

"""Strategy in Python: each pricing rule is a plain function, and a dict picks one by name."""
import math


def hourly(minutes):
    return 20 * math.ceil(minutes / 60)


def flat(minutes):
    return 100


def first_hour_free(minutes):
    return 0 if minutes <= 60 else 30 * math.ceil((minutes - 60) / 60)


RULES = {"hourly": hourly, "flat": flat, "first-hour-free": first_hour_free}

cases = [line.split() for line in open("cases.txt") if line.strip() and not line.startswith("#")]
fees = [RULES[rule](int(minutes)) for rule, minutes, _ in cases]
passed = sum(fee == int(expected) for fee, (_, _, expected) in zip(fees, cases))
print("fees:", *fees)
print(f"{passed} of {len(cases)} cases pass")

Output

fees: 20 20 40 60 100 100 0 30 90
9 of 9 cases pass

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 strategy.py

strategy/cases.txt

# rule minutes expected-fee: the shared test table that every version of the parking-fee strategies must pass
hourly 1 20
hourly 60 20
hourly 61 40
hourly 150 60
flat 5 100
flat 600 100
first-hour-free 60 0
first-hour-free 61 30
first-hour-free 185 90

The same rules in TypeScript, Java and C++, built and run by one session that prints each compiler’s version:

The same Strategy in TypeScript, Java and C++

strategy/session.mjs

// Builds and runs the TypeScript, Java and C++ versions as you would in a terminal, and prints the session.
// TypeScript runs on this Node.js, which strips the types. Java is Homebrew's OpenJDK, built with --release 25 so
// that the code may use nothing newer than Java 25; C++ is the system clang++. Version lines are printed first,
// without release dates. A command that fails stops the session with exit status 1.
import { spawnSync } from 'node:child_process';

const PATH = ['/opt/homebrew/opt/openjdk/bin', '/usr/bin', '/bin'].join(':');
const run = (command) => spawnSync('/bin/sh', ['-c', command], { encoding: 'utf8', env: { ...process.env, PATH } });
const undated = (text) => text.split('\n')[0].replace(/\s+\d{4}-\d{2}-\d{2}/, '');

console.log(`# Node.js ${process.version}`);
console.log(`# ${undated(run('java -version 2>&1').stdout)}`);
console.log(`# ${undated(run('clang++ --version').stdout)}`);
for (const command of [
  `${process.execPath} strategy.ts`,
  'javac --release 25 -d classes Strategy.java',
  'java -cp classes Strategy',
  'clang++ -std=c++23 -Wall -Wextra -O1 strategy.cpp -o strategy',
  './strategy',
]) {
  console.log(`$ ${command.replace(process.execPath, 'node')}`);
  const result = run(`${command} 2>&1`);
  process.stdout.write(result.stdout);
  if (result.status !== 0) {
    console.log(`[exit status ${result.status}]`);
    process.exit(1);
  }
}

Output

# Node.js v24.21.0
# openjdk version "27"
# Apple clang version 21.0.0 (clang-2100.3.34.2)
$ node strategy.ts
fees: 20 20 40 60 100 100 0 30 90
9 of 9 cases pass
$ javac --release 25 -d classes Strategy.java
$ java -cp classes Strategy
fees: 20 20 40 60 100 100 0 30 90
9 of 9 cases pass (classes)
fees: 20 20 40 60 100 100 0 30 90
9 of 9 cases pass (lambdas)
$ clang++ -std=c++23 -Wall -Wextra -O1 strategy.cpp -o strategy
$ ./strategy
fees: 20 20 40 60 100 100 0 30 90
9 of 9 cases pass

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node session.mjs

TypeScript · strategy/strategy.ts

// Strategy in TypeScript: a function type is the interface, and an object maps names to strategies.
import { readFileSync } from 'node:fs';

type PricingRule = (minutes: number) => number;

const startedHours = (minutes: number): number => Math.ceil(minutes / 60);

const rules: Record<string, PricingRule> = {
  hourly: (minutes) => 20 * startedHours(minutes),
  flat: () => 100,
  'first-hour-free': (minutes) => (minutes <= 60 ? 0 : 30 * startedHours(minutes - 60)),
};

const cases = readFileSync('cases.txt', 'utf8')
  .split('\n')
  .filter((line) => line.trim() !== '' && !line.startsWith('#'))
  .map((line) => line.split(' '));
const fees = cases.map(([rule, minutes]) => rules[rule](Number(minutes)));
const passed = fees.filter((fee, i) => fee === Number(cases[i][2])).length;
console.log('fees:', ...fees);
console.log(`${passed} of ${cases.length} cases pass`);

Java · strategy/Strategy.java

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;

/** Strategy in Java: the classic form, an interface and one class per rule, then the same rules as lambdas. */
public class Strategy {

    interface PricingRule {
        int fee(int minutes);
    }

    static int startedHours(int minutes) {
        return (minutes + 59) / 60;
    }

    static final class Hourly implements PricingRule {
        public int fee(int minutes) {
            return 20 * startedHours(minutes);
        }
    }

    static final class Flat implements PricingRule {
        public int fee(int minutes) {
            return 100;
        }
    }

    static final class FirstHourFree implements PricingRule {
        public int fee(int minutes) {
            return minutes <= 60 ? 0 : 30 * startedHours(minutes - 60);
        }
    }

    // A one-method interface is a functional interface, so each rule can also be a lambda.
    static final Map<String, PricingRule> CLASSES =
            Map.of("hourly", new Hourly(), "flat", new Flat(), "first-hour-free", new FirstHourFree());
    static final Map<String, PricingRule> LAMBDAS = Map.of(
            "hourly", minutes -> 20 * startedHours(minutes),
            "flat", minutes -> 100,
            "first-hour-free", minutes -> minutes <= 60 ? 0 : 30 * startedHours(minutes - 60));

    public static void main(String[] args) throws Exception {
        List<String[]> cases = Files.readAllLines(Path.of("cases.txt")).stream()
                .filter(line -> !line.isBlank() && !line.startsWith("#"))
                .map(line -> line.split(" "))
                .toList();
        for (Map<String, PricingRule> rules : List.of(CLASSES, LAMBDAS)) {
            StringBuilder fees = new StringBuilder("fees:");
            int passed = 0;
            for (String[] c : cases) {
                int fee = rules.get(c[0]).fee(Integer.parseInt(c[1]));
                fees.append(' ').append(fee);
                if (fee == Integer.parseInt(c[2])) passed++;
            }
            System.out.println(fees);
            System.out.println(passed + " of " + cases.size() + " cases pass (" + (rules == CLASSES ? "classes" : "lambdas") + ")");
        }
    }
}

C++ · strategy/strategy.cpp

// Strategy in C++23: std::function holds any callable with the right signature, so each rule is a lambda.
#include <fstream>
#include <functional>
#include <map>
#include <print>
#include <sstream>
#include <string>
#include <vector>

using PricingRule = std::function<int(int)>;

int started_hours(int minutes) { return (minutes + 59) / 60; }

int main() {
    const std::map<std::string, PricingRule> rules{
        {"hourly", [](int minutes) { return 20 * started_hours(minutes); }},
        {"flat", [](int) { return 100; }},
        {"first-hour-free", [](int minutes) { return minutes <= 60 ? 0 : 30 * started_hours(minutes - 60); }},
    };
    std::ifstream file{"cases.txt"};
    std::string line;
    std::vector<int> fees;
    int cases = 0, passed = 0;
    while (std::getline(file, line)) {
        if (line.empty() || line.starts_with('#')) continue;
        std::istringstream fields{line};
        std::string rule;
        int minutes = 0, expected = 0;
        fields >> rule >> minutes >> expected;
        const int fee = rules.at(rule)(minutes);
        fees.push_back(fee);
        ++cases;
        if (fee == expected) ++passed;
    }
    std::print("fees:");
    for (int fee : fees) std::print(" {}", fee);
    std::println("\n{} of {} cases pass", passed, cases);
}

All four print the same nine fees and pass all nine cases. What differs is the ceremony around each rule:

  • Java shows the classic form first: an interface, PricingRule, and one class per rule. Because the interface has exactly one abstract method, it is a functional interface, so the second map holds the same rules as lambdas. The session compiles with javac --release 25, which accepts only the Java 25 language and API.
  • TypeScript needs no classes at all. The interface is a function type, (minutes: number) => number, and because TypeScript’s types are structural, any function of that shape fits.
  • C++ stores each lambda in a std::function<int(int)>, which can hold any callable with that signature. A template parameter would avoid the indirection, at the price of one compiled copy of the lot per rule type.
  • Python uses functions and a dictionary, as above; a typing.Protocol could name the signature for a type checker.

Go and Rust, the other two languages of this track, need no classes either. In Go a rule is a value of a function type such as func(minutes int) int, often a closure that captures its rate. In Rust it is either a generic parameter bounded by Fn(u32) -> u32, compiled into one specialised copy per closure type, or a Box<dyn Fn(u32) -> u32>, a trait object that is called through a table of function pointers at run time. A HashMap of rules needs the second form, because every closure has its own type and a generic parameter stands for one type at a time.

Classes are still the better shape when a strategy has more than one operation, a fee and a description say, when it keeps state between calls, or when it must be configured, saved or listed by name. Then an interface with classes, or a small record holding several functions, says more than a bare function.

How the six languages express the building blocks

The same four questions decide how a pattern looks in each language: how an interface is declared, how one is chosen while the program runs, how code is reused without inheritance, and whether the language can describe a closed set of cases and check that every case is handled.

Interfaces, and choosing behaviour at run time
Criterion InterfacesChoosing behaviour at run time
Java 25 interface, declared with implementsvirtual methods; lambdas for one-method interfaces
Python 3.14 duck typing; abc.ABC or typing.Protocolany object with the method; functions
TypeScript structural: any value of the right shapefunctions and objects as values
Go 1.27 implicit: having the methods is enoughinterfaces; functions and closures
Rust 1.99 traits, implemented explicitlydyn Trait; generics choose at compile time
C++23 abstract classes; concepts for templatesvirtual functions, std::function
Reuse without inheritance, and closed sets of cases
Criterion Reuse without inheritanceClosed sets of cases
Java 25 composition; default methodssealed types, records, pattern-matching switch
Python 3.14 composition, modules, functionsmatch statements, with no exhaustiveness check
TypeScript composition, modulesdiscriminated unions, checked by narrowing
Go 1.27 struct embeddingno sum types: an interface and a type switch
Rust 1.99 traits with default methodsenums with exhaustive match
C++23 composition, templatesstd::variant with std::visit

A few entries change patterns more than the others:

  • Sum types against Visitor. Visitor adds operations to a fixed hierarchy through double dispatch. A language with sum types writes each operation as one function with an exhaustive match instead: a Rust enum, a Java sealed interface with a pattern-matching switch, a TypeScript union with a shared tag. Adding a case then makes the compiler point at every match to update, the same trade-off as Visitor with much less code.
  • Go has no inheritance. Effective Go puts it plainly: Go has no type-driven subclassing, and embedding lets a struct borrow another type’s methods. Patterns built on subclassing, such as Template Method, become a struct that holds a function or a small interface for the steps that vary.
  • Go 1.27 added generic methods, but interface methods may still not declare type parameters, and a generic method cannot implement an interface method. A Visitor interface with a generic Accept that returns any type is therefore still impossible in Go.
  • Rust has no inheritance either, and its ownership rules shape patterns that share objects: an Observer that keeps subscribers alive must decide who owns them.
  • C++23 on Apple clang 21. The C++23 standard has std::generator, which makes an Iterator a coroutine, and std::move_only_function, but the libc++ that ships with Apple clang 21 implements neither yet, and both fail to compile there. This track’s C++ examples avoid them.

TypeScript on Node.js: only erasable syntax

Node.js 24 runs .ts files directly by replacing every type annotation with whitespace, without checking the types; type stripping is stable since Node.js 24.12.0. Syntax that needs generated JavaScript, such as enums, namespaces with runtime code and parameter properties, cannot be stripped, so Node.js refuses it. Here is a pricing rule written with a parameter property, and the same class with only erasable syntax:

A parameter property against an erasable class

typescript/run_ts.mjs

// Runs both TypeScript files on this Node.js, which executes .ts files by stripping their types, and prints what
// each run shows: the output, or the first line of the error and the exit status.
import { spawnSync } from 'node:child_process';

console.log(`# Node.js ${process.version}`);
for (const file of ['param_property.ts', 'erasable.ts']) {
  console.log(`$ node ${file}`);
  const result = spawnSync(process.execPath, [file], { encoding: 'utf8' });
  if (result.status === 0) {
    process.stdout.write(result.stdout);
  } else {
    const error = result.stderr.split('\n').find((line) => line.includes('ERR_'));
    console.log(error ?? result.stderr.trim());
    console.log(`[exit status ${result.status}]`);
  }
}

Output

# Node.js v24.21.0
$ node param_property.ts
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript parameter property is not supported in strip-only mode
[exit status 1]
$ node erasable.ts
40

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node run_ts.mjs

typescript/param_property.ts

// A pricing rule as a class with a parameter property: TypeScript turns `private readonly rate` into a field and an
// assignment, code that Node.js would have to generate, so a run that only strips types refuses it.
class HourlyRate {
  constructor(private readonly rate: number) {}

  fee(minutes: number): number {
    return this.rate * Math.ceil(minutes / 60);
  }
}

console.log(new HourlyRate(20).fee(61));

typescript/erasable.ts

// The same class with only erasable syntax: delete the types and valid JavaScript is left.
class HourlyRate {
  readonly #rate: number;

  constructor(rate: number) {
    this.#rate = rate;
  }

  fee(minutes: number): number {
    return this.#rate * Math.ceil(minutes / 60);
  }
}

console.log(new HourlyRate(20).fee(61));

TypeScript 5.8 added the --erasableSyntaxOnly option, which reports such syntax when the compiler checks your code, before anything runs. This track’s TypeScript uses only erasable syntax: fields assigned in the constructor instead of parameter properties, and unions of string literals or plain objects instead of enums.

Reading the examples in this track

Most pattern lessons show one program in several languages, as tabs of one example. Python and JavaScript examples run in your browser with the Run button, and every example shows the output it printed, with the command and the toolchain version under it. Programs in compiled languages come with a session file that builds and runs them the way you would in a terminal; its output starts with each compiler’s version line and shows every command it typed. When two languages need different code for the same idea, the lesson says why.

Practise: a Java-style Strategy, ported properly

shipping.py was ported from Java line by line: an abstract class and three one-method subclasses. Rewrite it with one function per rule and a dictionary. The sample tests check costs the starter was written for and weights it never saw, call the rules directly, add a rule from outside, and confirm that no class is left.

Exercise · Easy · Python

Port a Java-style Strategy to idiomatic Python

shipping.py was ported from Java line by line: an abstract ShippingRule class, one subclass per rule, each with a single cost method, and a RULES dict of instances. Python has first-class functions, so the three classes carry no information that three functions would not. Rewrite the module the Python way:

1. One plain function per rule, each taking the weight in grams and returning the cost: standard is 40 plus 10 for every started kilogram, express is 90 plus 15 for every started kilogram, and pickup is always 0. 2. RULES maps the names "standard", "express" and "pickup" to those functions. 3. shipping_cost(method, grams) looks the method up in RULES when it is called and returns the cost; an unknown method raises KeyError.

The sample tests check costs for weights the starter was written for and for weights it never saw, call the functions in RULES directly, add a new rule to RULES from the test file, and read your source: no class may be left in the module.

Starter code · shipping.py

"""Shipping costs, ported from Java line by line: an abstract rule and one subclass per rule."""
import math
from abc import ABC, abstractmethod


class ShippingRule(ABC):
    @abstractmethod
    def cost(self, grams):
        """The cost of shipping a parcel of this many grams."""


class Standard(ShippingRule):
    def cost(self, grams):
        return 40 + 10 * math.ceil(grams / 1000)


class Express(ShippingRule):
    def cost(self, grams):
        return 90 + 15 * math.ceil(grams / 1000)


class Pickup(ShippingRule):
    def cost(self, grams):
        return 0


RULES = {"standard": Standard(), "express": Express(), "pickup": Pickup()}


def shipping_cost(method, grams):
    return RULES[method].cost(grams)
The sample tests · test_shipping.py
import ast
import inspect

import shipping
from shipping import RULES, shipping_cost


def test_costs_the_starter_was_written_for():
    """the same costs as the Java-style port"""
    assert shipping_cost("standard", 500) == 50
    assert shipping_cost("standard", 1000) == 50
    assert shipping_cost("standard", 1001) == 60
    assert shipping_cost("express", 2500) == 135
    assert shipping_cost("pickup", 9000) == 0


def test_weights_it_never_saw():
    """started kilograms for any weight"""
    assert shipping_cost("standard", 12345) == 170
    assert shipping_cost("express", 1) == 105
    assert shipping_cost("express", 7000) == 195
    assert shipping_cost("pickup", 1) == 0


def test_rules_are_plain_functions():
    """RULES maps each name to a function that takes grams"""
    assert set(RULES) == {"standard", "express", "pickup"}
    assert all(inspect.isfunction(rule) for rule in RULES.values()), "store the functions themselves in RULES"
    assert RULES["express"](1000) == 105
    assert RULES["standard"](2000) == 60


def test_a_new_rule_joins_from_outside():
    """shipping_cost looks the rule up when it is called"""
    RULES["drone"] = lambda grams: 200
    try:
        assert shipping_cost("drone", 100) == 200
    finally:
        del RULES["drone"]


def test_unknown_method():
    """an unknown method raises KeyError"""
    try:
        shipping_cost("teleport", 100)
    except KeyError:
        return
    raise AssertionError("shipping_cost('teleport', 100) should raise KeyError")


def test_no_class_hierarchy_is_left():
    """first-class functions replace the one-method classes"""
    tree = ast.parse(inspect.getsource(shipping))
    classes = [node.name for node in ast.walk(tree) if isinstance(node, ast.ClassDef)]
    assert classes == [], f"classes left in shipping.py: {classes}"
A hint

Each subclass's cost method is already the body of a function: move it out of its class, give the function the rule's name and drop self. "Every started kilogram" is math.ceil(grams / 1000), which counts 1001 grams as two kilograms. Then RULES holds the functions themselves, without calling them, and shipping_cost becomes one line that calls RULES[method] with the weight.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Python Online Compiler Run the Python Strategy and try a fourth pricing rule. JavaScript & TypeScript Online Compiler Rewrite the TypeScript rules as JavaScript functions and run them in the browser.

Check yourself

8 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 8 Norvig's study of the Design Patterns book found how many of the 23 patterns simpler or invisible in Lisp or Dylan, for at least some uses of each?

    Choose one answer.

    Show the answer to question 1

    Answer: 16

    16 of 23: six through first-class types, four through first-class functions, two each through macros and method combination, and one each through multimethods and modules. The other seven keep most of their shape.

  2. Question 2 of 8 Which patterns did Norvig list as simpler because of first-class functions?

    Choose every answer that is right.

    Show the answer to question 2

    Answer:

    • Strategy
    • Visitor
    • Command
    • Template Method

    Command, Strategy, Template Method and Visitor. Each one wraps behaviour in an object so that it can be passed around, which a function value does directly. Facade was listed under modules; Singleton was not among the 16.

  3. Question 3 of 8 How does a Go type implement an interface?

    Choose one answer.

    Show the answer to question 3

    Answer: By having the interface's methods; nothing declares the relationship

    Effective Go says a type need not declare that it implements an interface; it does so by implementing the methods. Embedding promotes methods but is composition, and Go has no inheritance between structs.

  4. Question 4 of 8 A Rust parking lot must keep pricing rules of different closure types in one HashMap. Which form do the values need?

    Choose one answer.

    Show the answer to question 4

    Answer: Box<dyn Fn(u32) -> u32>, a trait object, because a generic parameter stands for one concrete type at a time

    Every closure has its own type. A generic bound is monomorphised to one concrete type, which is fast but cannot mix types in one collection; a trait object can, at the cost of dynamic dispatch through a vtable.

  5. Question 5 of 8 What does Node.js 24 do with a .ts file whose constructor declares private readonly rate: number as a parameter property?

    Choose one answer.

    Show the answer to question 5

    Answer: It stops with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, because the syntax needs generated code, not only removed types

    Node.js replaces types with whitespace and does no type checking. A parameter property also creates a field and an assignment, which stripping cannot produce, so the run fails, as this lesson's recorded output shows. TypeScript's erasableSyntaxOnly option reports such syntax before you run it.

  6. Question 6 of 8 Why can each pricing rule in Strategy.java also be written as a lambda?

    Choose one answer.

    Show the answer to question 6

    Answer: PricingRule has exactly one abstract method, so it is a functional interface that a lambda can implement

    A Java interface with one abstract method is a functional interface, and a lambda or a method reference can stand for it. An interface with two operations, a fee and a description say, would still need classes or records.

  7. Question 7 of 8 A Rust enum lists every kind of parking ticket, and each operation is a function with a match. What happens when you add a new kind of ticket?

    Choose one answer.

    Show the answer to question 7

    Answer: Every match that does not handle the new variant fails to compile, so the compiler lists each operation to update

    Rust's match is exhaustive. That is the same trade-off as Visitor, with less code: adding an operation is easy, one new function, while adding a case touches every operation, and the compiler finds them all.

  8. Question 8 of 8 Go 1.27 adds methods with their own type parameters. Why can a generic Visitor interface still not be declared?

    Choose one answer.

    Show the answer to question 8

    Answer: Interface methods may not declare type parameters, and generic methods cannot implement interface methods

    The release notes say exactly that. A method such as Accept[R any](v Visitor[R]) R can exist on a concrete type, but no interface can require it, which a Visitor over a set of types would need.

Interview questions

Fresher · Why do some design patterns disappear in languages with first-class functions?

Because those patterns exist to pass one piece of behaviour around, and in languages without functions as values the only way to do that is an object of a class with one method. Strategy, Command, Template Method and Visitor all have that shape: an interface with one operation and a class per variant. When functions are values, the variant is just a function, a lambda or a closure, stored in a variable or a dictionary and called later, so the interface and the classes vanish. The pattern’s idea stays, a family of interchangeable behaviours chosen at run time, but the code shrinks to a few lines. Peter Norvig found 16 of the 23 classic patterns simpler in dynamic languages for this kind of reason. Classes come back when the behaviour needs several operations or its own state.

Key takeaways

  • A pattern’s problem is the same in every language; the amount of code it needs depends on the language’s features.
  • Norvig found 16 of the 23 classic patterns simpler or invisible in Lisp or Dylan; first-class functions alone simplify Command, Strategy, Template Method and Visitor.
  • Strategy is an interface and classes in classic Java, a function type in TypeScript, std::function in C++, a function in Python and Go, and a generic bound or a boxed closure in Rust.
  • Sum types with exhaustive matching replace much of Visitor; Go and Rust have no inheritance, so subclass-based patterns become functions or small interfaces.
  • Node.js runs TypeScript by stripping types, so parameter properties and enums fail; --erasableSyntaxOnly catches them first.

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.