One team adds a factory, a builder and a decorator to a service that has a single pricing rule, and the review says the code follows the patterns. Another team writes the same switch on a shipping method in five files and does not see that one decision is now made in five places. Both teams own the same book.

The 23 patterns are names for recurring design problems, each with a shape of solution and a price, and most of them are one move: an interface placed between two things so that one can vary without the other knowing. A pattern earns its price only where that variation is real.

This part takes from Part 3 the mechanisms of objects (encapsulation, interfaces and composition) and the discount policies of Stage 05, the material every pattern below is made of. It is a reference: read the next two sections, then use the contents list of this page.

Where the book came from

The word and the idea come from the architecture of buildings: A Pattern Language: Towns, Buildings, Construction, by Christopher Alexander, Sara Ishikawa and Murray Silverstein, with Max Jacobson, Ingrid Fiksdahl-King and Shlomo Angel, was published by Oxford University Press in 1977. Alexander's The Timeless Way of Building followed in 1979. Software patterns take the idea and the name from these books.

Kent Beck, then at Apple Computer, and Ward Cunningham, at Tektronix, wrote a short text dated 17 September 1987 that says: "We outline our adaptation of Pattern Language to object-oriented programming." It is often cited as a paper presented at OOPSLA 1987. It was Technical Report CR-87-43, submitted to a workshop of OOPSLA-87, and is not in that conference's main proceedings.

The catalogue grew out of one framework. The four authors' first joint paper says so: "This work has its roots in Gamma's thesis [11], which abstracted design patterns from the ET++ framework." Springer published that thesis in Berlin in 1992, with Design-Muster in its German title. The joint paper, "Design Patterns: Abstraction and Reuse of Object-Oriented Design", was written for the proceedings of ECOOP '93, a year before the book. Erich Gamma was then at Taligent, Richard Helm and John Vlissides at IBM's T. J. Watson Research Center, Ralph Johnson at the University of Illinois.

Design Patterns: Elements of Reusable Object-Oriented Software, by Gamma, Helm, Johnson and Vlissides, was published by Addison-Wesley Professional on 31 October 1994 with a 1995 copyright date, which is why both years circulate. The first edition has 416 pages, belongs to the Addison-Wesley Professional Computing Series and carries ISBN-10 0-201-63361-2 and ISBN-13 978-0-201-63361-0. It catalogues 23 patterns.

A pattern in that catalogue is a named description of a design problem that recurs in object-oriented programs, of an arrangement of classes and objects that answers it, and of what the arrangement costs. The book classifies each pattern twice: by purpose (creational, structural or behavioral) and by scope (class or object). The authors' tutorial slides reproduce the table. Only four entries have class scope: Factory Method, Adapter in its class form, Interpreter and Template Method. In our reading, class scope is the inheritance kind and object scope the composition kind, so the table already leans the way the book's advice does.

That advice is in the introduction, which states two principles of reusable object-oriented design. An interview with Gamma gives them as "Program to an interface, not an implementation." and "favor object composition over class inheritance". Stage 05 applied both.

The four authors are commonly called the Gang of Four, and the book GoF. The usage is established: the publisher's page links to a retrospective under that name, and the 2005 interview speaks of "the GoF book". Who first used the nickname, and when, is not established.

YearWhoWhat, in the original termsSource
1977Alexander and colleaguesA Pattern Language: Towns, Buildings, ConstructionAlexander's site
1979AlexanderThe Timeless Way of BuildingAlexander's site
1987Beck and Cunningham"Using Pattern Languages for Object-Oriented Programs", Technical Report CR-87-43, 17 SeptemberCunningham's site
1992GammaThesis on ET++ published by Springer: Design-MusterJoint paper
1993Gamma, Helm, Johnson, Vlissides"Design Patterns: Abstraction and Reuse of Object-Oriented Design", ECOOP '93Authors' preprint
1994Gamma, Helm, Johnson, VlissidesDesign Patterns, published 31 October, copyright 1995Publisher's page
1996Norvig"Design Patterns in Dynamic Programming", Object World, 5 MayNorvig's site
2009Gamma, Helm, Johnson"Design Patterns 15 Years Later", interview of 22 OctoberInformIT

How to read a pattern

A pattern names a problem that keeps returning, the shape of a solution, and the consequences of choosing it. It is not a library. It is not a goal: no design is better for containing more of them. It is not a measure of quality: Stage 03 of this series contains none and suits its problem.

The name is the most useful part. Calling the shipping rule a strategy tells a colleague in one word that the rule is passed in and can be replaced. That works only if the name is kept for the problem it was given to.

For each of the 23, this article gives four things:

  • The problem: what goes wrong without it.
  • The shape: the solution in this series' example, with the region that holds the code.
  • The cost: what the pattern adds, and how it is misused.
  • The modern form: what C# offers in its place, where that is an honest simplification. Eleven patterns have one, in a region named pattern-<name>-modern. Twelve do not.

No description below is the book's wording, and the examples are not the book's.

Creational patterns

These five are about who decides which object is created, and when.

Abstract Factory

Some objects are only correct together. A market's tax rule and its shipping rule belong to each other, and a checkout that takes one from each of two markets computes a total that is wrong and looks right.

In pattern-abstract-factory (src/Patterns/Creational/AbstractFactory.cs), IMarketFactory has one creation method for each member of the family. DomesticMarket returns 23% tax with 7.50 shipping, ExportMarket no tax with 25.00 shipping, and Checkout asks the market it is given for both. The rule classes are internal, so nothing outside can build a mismatched pair.

It earns its place when there are at least two families and mixing them is a defect. The cost arrives when the family grows: a third kind of rule changes the interface and every factory. With one market it is ceremony. The example offers no shorter modern form.

Builder

An order has parts that arrive one at a time, some optional, and a rule that can only be checked when all are present. A constructor with every part as a parameter is hard to read.

In pattern-builder (src/Patterns/Creational/Builder.cs), AddLine, ForTier and WithCoupon each record one part and return the builder, so the calls chain. Build refuses an order with no lines and otherwise returns an immutable Order.

It earns its place when construction really is stepwise, or a rule spans several parts. The cost is a second class that mirrors the first, and errors that move from compile time to the call of Build.

pattern-builder-modern is nine lines: a class whose Lines member is required and whose other members have defaults. The compiler refuses an incomplete object, but it checks presence, not content: an empty list compiles, and that is the check the builder kept.

Factory Method

A class owns an algorithm that needs an object, and should not decide which object. Here a checkout computes a total without knowing which discount applies.

In pattern-factory-method (src/Patterns/Creational/FactoryMethod.cs) the abstract Checkout calls a protected abstract CreateDiscount. StandardCheckout overrides it to return no discount and GoldCheckout to return 7%. The decision is taken by inheritance.

It earns its place inside a framework whose base class you extend anyway. The cost is a subclass for every choice. The name is also used loosely for any static method that creates something: Order.Place in Stage 09 is such a method, and a useful one, but no subclass decides anything there.

pattern-factory-method-modern is six lines. FuncCheckout takes a Func<IDiscount>, so the decision that needed a subclass is an argument and one sealed class serves every tier. FactoryMethod_and_a_func_factory_give_the_same_totals proves the totals equal.

Prototype

A new object is mostly like one that exists, and building it from nothing repeats the configuration.

pattern-prototype (src/Patterns/Creational/Prototype.cs) is eight lines. OrderTemplate holds a tier and a list of SKUs, and Clone returns a new template with its own copy of the list. Prototype_a_clone_can_change_without_changing_the_original checks that the template is untouched.

It earns its place for templates, repeat orders and configured defaults. The cost is that the depth of the copy is decided member by member, and again whenever a member is added. A list shared by accident is a defect that shows up far from its cause.

pattern-prototype-modern uses a record copied with with; Microsoft's record reference lists nondestructive mutation among the built-in behaviour. The copy is shallow, and safe here only because OrderSnapshot keeps its SKUs in an ImmutableArray<string>.

Singleton

One instance of a class must exist, and any code must be able to find it. The classic form is pattern-singleton (src/Patterns/Creational/Singleton.cs):

C#
public sealed class PriceList
{
    // Lazy<T> creates the instance on first use and is safe across threads.
    private static readonly Lazy<PriceList> Lazy = new(() => new PriceList());

    private readonly Dictionary<string, decimal> _prices = new()
    {
        ["NOTEBOOK"] = 25.00m,
        ["PEN-SET"] = 49.90m,
    };

    private PriceList() { }

    public static PriceList Instance => Lazy.Value;

    public decimal PriceOf(string sku) => _prices[sku];
}

The constructor is private, Lazy<PriceList> creates the only instance on first use, safely across threads, and Instance is how every caller reaches it.

The single instance is rarely the problem. The global access is. Any code can reach PriceList.Instance, so no signature shows who depends on it, a test cannot replace it, and any mutable state it gains is shared by every caller.

There is no -modern region because the modern form is not in the class. In host-container-stage10 (src/Pricing.Host/ContainerRoot.cs) the repository is registered with AddSingleton and injected. Microsoft's service lifetimes page says: "Services can be registered with a transient, scoped, or singleton lifetime." The lifetime is the same and the dependency is visible. We would keep the classic form for an immutable table, and for little else.

Structural patterns

These seven are about how objects are put together so that the parts can be exchanged.

Adapter

An existing class does what you need behind an interface you cannot use, and you cannot change the class. The pricing code wants a tax rate for a country code, as a fraction. The table that exists answers in basis points for a numeric region.

In pattern-adapter (src/Patterns/Structural/Adapter.cs), LegacyTaxTableAdapter implements ITaxRates, holds a LegacyTaxTable, turns PT into region 351 and divides 2300 basis points by 10,000. This is the object form; the book also lists a class form.

It earns its place at the edge of code you do not own, where it keeps a foreign vocabulary out of yours. The cost is in the translation: an unknown country becomes region 0 and a rate of zero, a business decision hidden in a conversion.

There is no shorter modern form. The adapters of Stage 10 (stage10-adapters) use the word more widely, for implementations of interfaces the application owns: see Part 7.

Bridge

Two things vary independently, and inheritance alone needs a class for every combination. Two kinds of price document and two output formats would be four subclasses.

In pattern-bridge (src/Patterns/Structural/Bridge.cs) the abstract PriceDocument holds an IRenderer. Its Print asks the subclass for a title and rows and hands them to the renderer. Quote supplies one row and Invoice three; TextRenderer and CsvRenderer know nothing about either. Bridge_documents_and_renderers_combine_freely prints all four combinations.

It earns its place when both sides really grow. The cost is the interface between them, which must suit every pair: here every document is reduced to a title and labelled amounts, and a document that needs a logo breaks that agreement for all. With one renderer it is a guess.

The example offers no modern form. Structurally a bridge is a strategy held by an abstract class; the separate name records the intent.

Composite

Some structures are trees, and the caller should not care whether it holds one thing or a group. A bundle is sold like a product, and may contain bundles. pattern-composite (src/Patterns/Structural/Composite.cs):

C#
public interface IOrderItem
{
    decimal Price { get; }
}

// The leaf.
public sealed record Product(string Sku, decimal Price) : IOrderItem;

// The composite: an item made of items, which may themselves be bundles.
public sealed class Bundle(decimal discountRate, params IOrderItem[] items) : IOrderItem
{
    public decimal Price => items.Sum(item => item.Price) * (1m - discountRate);
}

Product is the leaf. Bundle holds items, which may be bundles, and its price is the sum of theirs less its own discount. The caller of Price never asks which kind it has.

It earns its place wherever the structure really is recursive: bundles, menus, folders, the rule trees of Interpreter below. The cost sits in the shared interface. An operation that only suits a group, such as adding a child, either goes on the interface, where a leaf must refuse it, or stays off it, where callers must ask for the type. The example avoids the question by fixing the children at construction.

Modern C# has nothing shorter to offer, and at thirteen lines it does not need to.

Decorator

An object needs extra responsibilities, in combinations, and a subclass for every combination does not scale. Discount and tax are each a small adjustment to a price. pattern-decorator (src/Patterns/Structural/Decorator.cs):

C#
public interface IPricer
{
    decimal Price(decimal subtotal);
}

public sealed class BasePricer : IPricer
{
    public decimal Price(decimal subtotal) => subtotal;
}

public sealed class WithDiscount(IPricer inner, decimal rate) : IPricer
{
    public decimal Price(decimal subtotal) => inner.Price(subtotal) * (1m - rate);
}

public sealed class WithTax(IPricer inner, decimal rate) : IPricer
{
    public decimal Price(decimal subtotal) => inner.Price(subtotal) * (1m + rate);
}

Each wrapper has the interface of what it wraps, holds it, and adjusts its answer. The modern form, pattern-decorator-modern, drops the classes:

C#
// A pipeline of delegates, the shape of web middleware. Each step receives the next
// step and returns a function that does its own work before calling it.
public static class Pipeline
{
    public static Func<decimal, decimal> Build(params Func<Func<decimal, decimal>, Func<decimal, decimal>>[] steps)
    {
        Func<decimal, decimal> pipeline = amount => amount;
        for (int i = steps.Length - 1; i >= 0; i--)
        {
            pipeline = steps[i](pipeline);
        }
        return pipeline;
    }

    public static Func<Func<decimal, decimal>, Func<decimal, decimal>> Discount(decimal rate) =>
        next => amount => next(amount * (1m - rate));

    public static Func<Func<decimal, decimal>, Func<decimal, decimal>> Tax(decimal rate) =>
        next => amount => next(amount * (1m + rate));
}

Each step receives the next and returns a function, and Build composes them so that the first step given runs first. It is the shape of web middleware, which Microsoft defines so: "Middleware is software that's assembled into an app pipeline to handle requests and responses."

What changed: three classes and an interface became functions, and the steps are a list built at run time. What did not: the result, proved by Decorator_and_a_pipeline_of_delegates_give_the_same_price. The wrappers adjust the inner result on the way out and the steps adjust the amount on the way in, which agree here only because multiplication commutes.

The cost in both forms is that order matters and the call site does not show it. Wrap when the set of steps varies.

Facade

A subsystem has several parts and a required sequence, and every caller has to know both. Placing an order means reserving stock, finding the price and taking payment.

In pattern-facade (src/Patterns/Structural/Facade.cs), OrderDesk.Place takes a SKU and a quantity. If stock cannot be reserved it returns a Receipt marked not accepted, with nothing charged. Otherwise it prices the order, charges it and reports the total. Facade_one_call_reserves_stock_prices_the_order_and_takes_payment checks both outcomes.

It earns its place when many callers repeat one sequence. It simplifies and does not protect: Stock, Catalogue and Payments stay public. A class that sequences steps also owns their failures: the example has no path where payment fails after stock was reserved, and real code must.

There is no modern form. The PriceOrder use case of Stage 10 (stage10-use-case) plays the same part for the domain.

Flyweight

A program holds very many small objects that are mostly identical. Ten thousand order lines for one product do not need ten thousand copies of its description.

pattern-flyweight (src/Patterns/Structural/Flyweight.cs) separates two kinds of state. ProductInfo holds what is the same for every line of a product, and is shared. OrderLine holds a reference to it and the quantity. ProductCatalogue.Get creates the information for a SKU on first request and returns that one instance afterwards; Flyweight_lines_for_the_same_product_share_one_description checks it.

It earns its place only when a measurement shows that identical objects are a memory problem. The shared part must be immutable, or one line's change is every line's. The catalogue as written never forgets an entry and is not safe across threads.

pattern-flyweight-modern is seven lines: string.Intern returns the runtime's one shared instance for a given text, so equal SKUs read from many rows cost one string.

Proxy

Something must stand between a caller and an object, to decide whether and when the object is reached. A price source is expensive to ask, and the same SKU is asked for repeatedly.

In pattern-proxy (src/Patterns/Structural/Proxy.cs), CachingPriceSource implements IPriceSource, holds the real source and remembers each answer. RemotePriceSource counts its calls, and Proxy_the_real_source_is_asked_once_for_each_sku reads that counter.

The structure is the Decorator's: same interface, one inner object. The purpose differs. A decorator adds to what the object does. A proxy controls access to it: caching, loading on first use, checking permission, standing in for something on another machine.

The cost is that the caller cannot tell a remembered answer from a fresh one. This cache never expires, so a price that changes at the source is never seen again. A proxy for a remote object also makes a network call look local.

The example offers no modern form.

Behavioral patterns

These eleven are about how work and decisions are divided among objects.

Chain of Responsibility

A request should be handled by one of several candidates, and the sender should not know which. Who may approve a discount depends on its size.

In pattern-chain-of-responsibility (src/Patterns/Behavioral/ChainOfResponsibility.cs) the abstract Approver holds the next approver. Approve answers with its own title if it can approve the rate, passes the request on if it cannot, and answers "rejected" when nobody is left. SalesAssistant approves up to 5% and SalesManager up to 20%, the cap of the pricing rules.

It earns its place when the handlers and their order are configuration: approval ladders, filters. The cost is that the flow is in no one place. To know who answers you must know how the chain was assembled, and nothing guarantees that anyone does.

There is no -modern region. The nearest relative is the pipeline in pattern-decorator-modern: middleware in which a step may answer or call the next.

Command

A request has to outlive the call that makes it: to be undone, queued, logged or retried. For that it must be an object.

In pattern-command (src/Patterns/Behavioral/Command.cs), ICommand has Execute and Undo, and AddLine holds a cart and a SKU. History runs a command and pushes it on a stack; UndoLast pops one and undoes it.

It earns its place for undo, work queues and audit. The cost is a class for each operation, and undo itself: AddLine.Undo removes the last occurrence of its SKU, which is correct only because the history undoes in reverse order.

pattern-command-modern replaces the interface with a pair of Action delegates. Microsoft's definition: "A delegate is a type that represents references to methods with a particular parameter list and return type." The class returns when a command needs a name, has to be serialised, or is inspected before it runs.

Interpreter

Rules must be data: stored, and combined while the program runs. The pattern represents a small language as classes, one for each rule of its grammar, and evaluates a sentence by walking its tree.

pattern-interpreter (src/Patterns/Behavioral/Interpreter.cs) has a language that says when a promotion applies. SubtotalAtLeast and TierIs test a Basket; And, Or and Not combine rules. Gold tier with a subtotal of at least 1,000.00 is a tree of three objects. Turning text into the tree is a parser's work, outside the pattern.

It earns its place for a grammar this small. The cost grows with the grammar, and a language of your own is a commitment to error messages, documentation and versions. The structure is a Composite, and a second operation over the tree is where Visitor applies.

The example offers no modern form. Compare stage06-strategy-delegates, where OnlyWhen combines rules as functions: shorter, but a function cannot be stored or shown to a user.

Iterator

A caller needs the elements of a collection one after another, and should not learn how they are stored. Here a basket hands out its lines above a given amount.

In pattern-iterator (src/Patterns/Behavioral/Iterator.cs) the basket returns an IIterator with MoveNext and Current. A private nested class keeps the position, and the array is never exposed.

pattern-iterator-modern is nine lines against 26: the method returns IEnumerable<Line> and uses yield return, and the compiler writes the class. Microsoft describes the interface in the pattern's terms: "Exposes the enumerator, which supports a simple iteration over a collection of a specified type." Its history of C# lists iterators under version 2.0, of November 2005.

This is the clearest case of a pattern absorbed by a language: every foreach uses it and almost nobody writes it. The cost that remains is the modern form's: the body runs when the sequence is enumerated, and again on each enumeration.

Mediator

Several objects affect each other, and if each refers to the others, every new part must be wired to all of them. The pattern gives the interaction one owner.

The file src/Patterns/Behavioral/Mediator.cs has two regions. In pattern-mediator-colleagues, CartPanel and CouponBox know only ICheckoutMediator and call Changed when their value changes. In pattern-mediator, CheckoutScreen creates the parts and implements Changed: the label shows the subtotal, or 90% of it when the coupon is WELCOME10.

It earns its place in screens, dialogs and workflows where the interactions outnumber the parts. The cost is concentration: the mediator is where every rule of interaction goes, and it grows into the class nobody wants to change.

The example offers no modern form. The screen is a sketch of a user interface, not a pricing stage: it leaves out the first-order condition of the coupon rule, which every stage applies.

Memento

An object's state must be saved and restored later, and the object must not open its fields to whoever keeps the copy.

In pattern-memento (src/Patterns/Behavioral/Memento.cs) a Cart keeps a private list of SKUs and a coupon. Save returns a Memento holding a copy of both, and Restore puts them back. The memento's constructor and properties are internal: code in another assembly can hold one and hand it back, and nothing else.

It earns its place for undo and checkpoints over objects that guard their state. A command undoes by reversing an operation, a memento by restoring what was there. The cost is a copy for each save, and a saved state that must still fit the class after the class changes.

There is no -modern region. In our judgement the nearest relative is the immutable record of pattern-prototype-modern: when state is a value that is replaced and never edited, keeping the old value is the memento.

Observer

One object changes and others must react, and the first should not know who they are. A product's price changes, and a basket and a cache each care.

In pattern-observer (src/Patterns/Behavioral/Observer.cs) the product keeps a list of IPriceObserver, and SetPrice tells each one the SKU and the new price. It walks a copy of the list, so an observer may detach itself while being told.

pattern-observer-modern declares an event: += attaches, -= detaches and the delegate keeps the list. Microsoft's events guide: "Events enable a class or object to notify other classes or objects when something of interest occurs." For a stream of values with completion and errors there is IObservable<T>, documented as the observer design pattern.

The costs came with it. The subject holds every observer, so one that never detaches is never collected. As written, in both forms, an exception in one observer stops the rest from being told.

State

What an object may do next depends on where it is, and several operations each test the same condition. An order can be paid, shipped or cancelled, each only in some states.

In pattern-state and pattern-state-cancelled (src/Patterns/Behavioral/State.cs) the Order holds an IOrderState and starts as Placed. Pay, Ship and Cancel each ask the current state and keep the state it returns. Placed moves to Paid on payment and throws on shipping; Shipped refuses cancellation; Cancelled is the end.

It earns its place when the states carry real behaviour. The cost is visible here: four classes and twelve methods, and a new operation touches every class. For four states and three operations we would start with an enumeration and one switch expression, and move to classes when the states gain logic of their own.

The example offers no modern form. The structure is Strategy's; the difference is that here the states replace themselves.

Strategy

One algorithm among several must be chosen without the code that uses it changing. The series has already used this pattern: IDiscountPolicy in Stage 05 is a strategy. pattern-strategy (src/Patterns/Behavioral/Strategy.cs):

C#
public interface IShippingStrategy
{
    decimal CostFor(decimal subtotal);
}

public sealed class StandardShipping : IShippingStrategy
{
    public decimal CostFor(decimal subtotal) => subtotal >= 200m ? 0m : 7.50m;
}

public sealed class ExpressShipping : IShippingStrategy
{
    public decimal CostFor(decimal subtotal) => 15.00m;
}

public sealed class Checkout(IShippingStrategy shipping)
{
    public decimal Total(decimal subtotal) => subtotal + shipping.CostFor(subtotal);
}

Checkout adds a shipping cost and does not know which: rule 7 of the example, or a flat 15.00. In pattern-strategy-modern:

C#
// A strategy with one method is a function. The interface and its two classes become
// two methods, and the context takes a Func.
public static class Shipping
{
    public static decimal Standard(decimal subtotal) => subtotal >= 200m ? 0m : 7.50m;
    public static decimal Express(decimal subtotal) => 15.00m;
}

public sealed class FuncCheckout(Func<decimal, decimal> shippingCost)
{
    public decimal Total(decimal subtotal) => subtotal + shippingCost(subtotal);
}

What changed: the interface and two classes became two methods, and the context takes a Func<decimal, decimal>. What did not: the totals, proved by Strategy_and_a_func_give_the_same_totals at 100.00, 199.99 and 200.00. The history of C# dates lambda expressions to version 3.0, of November 2007.

A strategy with one method and no state is a function, and the delegate is then the better form. Keep the interface when the strategy has more than one member, carries state, needs a name, or is found by a container. IDiscountPolicy has a Name beside its method, which is why the delegate form in stage06-strategy-delegates needs a small record to carry it.

The cost is that someone must choose. An algorithm that never varies does not need a parameter.

Template Method

An algorithm has fixed steps in a fixed order, and some steps differ from case to case. The order is written once, in a base class, and subclasses supply the rest.

In pattern-template-method (src/Patterns/Behavioral/TemplateMethod.cs), PriceSummary.Render computes the tax and formats the line. DomesticSummary supplies 23% and ExportSummary zero. The base class calls the subclass, the direction Ralph Johnson and Brian Foote described in a 1988 paper on frameworks: "This inversion of control gives frameworks the power to serve as extensible skeletons."

It earns its place in frameworks. The series has one honest use elsewhere: the abstract test class in test-stage08-characterisation, which fixes the tests and lets a subclass supply the implementation. The cost is inheritance's: a class has one base, so one thing can vary this way, and the subclass depends on the order in which the base calls it.

pattern-template-method-modern hands the step in as a delegate: one sealed class, no hierarchy. It is the book's second principle applied to one of its own class-scope patterns.

Visitor

A set of classes is stable, and operations over them keep being added. Putting each in every class means editing every class each time. pattern-visitor (src/Patterns/Behavioral/Visitor.cs):

C#
public interface IItemVisitor<out T>
{
    T VisitGoods(Goods goods);
    T VisitService(Service service);
}

public abstract class Item
{
    public abstract T Accept<T>(IItemVisitor<T> visitor);
}

public sealed class Goods(decimal price, decimal weightKg) : Item
{
    public decimal Price => price;
    public decimal WeightKg => weightKg;
    public override T Accept<T>(IItemVisitor<T> visitor) => visitor.VisitGoods(this);
}

public sealed class Service(decimal price) : Item
{
    public decimal Price => price;
    public override T Accept<T>(IItemVisitor<T> visitor) => visitor.VisitService(this);
}

// A new operation is a new class; Goods and Service do not change.
public sealed class ShippingCost : IItemVisitor<decimal>
{
    public decimal VisitGoods(Goods goods) => goods.WeightKg * 1.50m;
    public decimal VisitService(Service service) => 0m;
}

Each item's Accept calls the visitor's method for its own type. An operation is then a class, and the items never change again. In pattern-visitor-modern:

C#
// Records and a switch expression. The private constructor closes the hierarchy to this
// file, and the operation is one function with no Accept methods. The compiler cannot
// yet prove the hierarchy closed, so the discard arm stays and a new case is found by a
// test, not by the compiler. The classic visitor does give that compile-time check.
public abstract record LineItem
{
    private LineItem() { }

    public sealed record GoodsItem(decimal Price, decimal WeightKg) : LineItem;
    public sealed record ServiceItem(decimal Price) : LineItem;
}

public static class Shipping
{
    public static decimal CostOf(LineItem item) => item switch
    {
        LineItem.GoodsItem goods => goods.WeightKg * 1.50m,
        LineItem.ServiceItem => 0m,
        _ => throw new UnreachableException(),
    };
}

What changed: Accept and the interface are gone, and the operation is one function that matches on the type. The history of C# dates pattern matching to version 7.0 (March 2017) and switch expressions to 8.0 (September 2019). What did not: the result, proved by Visitor_and_a_switch_expression_over_records_give_the_same_cost.

What the modern form gives up is in its comment: the compiler used here cannot prove the hierarchy closed, so a forgotten case is found by a test. In the classic form a new item type adds a method to the interface and every visitor fails to compile. The example targets .NET 10; Microsoft's history page lists closed hierarchies among the features of C# 15.

Either way, operations become cheap to add and item types expensive. Choose by which of the two grows.

What the patterns have in common

Set the 23 side by side and most of them are the same move. Something varies: an algorithm, a product, a format, a handler, a state. An interface or an abstract class is placed in front of it, and the code that uses it is written against that. The Strategy region shows the move with nothing else around it:

Text
Checkout ------uses------> IShippingStrategy
                              ^          ^
                              |          |   implements
                   StandardShipping   ExpressShipping

Replace the names and the same drawing serves Abstract Factory, Bridge, Proxy, Decorator, State, Command and Observer. Several pairs are identical in structure and differ only in purpose: Strategy and State, Decorator and Proxy, Composite and Interpreter. A class diagram cannot tell them apart. The name can, because it records why the indirection is there.

A few do something else. Singleton removes an indirection, which is the root of its cost. Flyweight is about sharing, Memento about protecting state, Prototype about copying, Builder and Facade about convenience.

Now the count. The eleven stages of the pricing example needed one pattern in earnest. Strategy carries the discount policies from Stage 05 on, in both forms, and the shipping policy of Stage 07:

Text
Stage 05 Interfaces            | mixed  | subtotal  2000.00 | discount   360.00 | shipping 0.00 | tax   377.20 | total  2017.20
Stage 06 Strategy (policies)   | mixed  | subtotal  2000.00 | discount   360.00 | shipping 0.00 | tax   377.20 | total  2017.20
Stage 06 Strategy (delegates)  | mixed  | subtotal  2000.00 | discount   360.00 | shipping 0.00 | tax   377.20 | total  2017.20

Beyond that there are traces. FixedTaxRateProvider in Stage 10 turns a raw number into the domain's type, a very small adapter. StageCatalogue fits every stage to one signature with lambdas, an adapter's job done without a class. PolicySelection in Stage 06 maps a name to a function that creates a policy. The host registers singletons in a container and contains no Singleton. The tests use one Template Method. Nothing in the stages called for the rest, which is why the patterns have a project of their own, with a small scenario invented for each.

In our judgement that is a normal proportion, not a weakness of the example. Pricing an order has one real axis of variation, the set of discount rules, and one pattern answers it. A design with twelve patterns over the same rules would compute the same totals and be harder to read.

Criticism and reassessment

Norvig, 1996

On 5 May 1996, at Object World, Peter Norvig, then at Harlequin, presented slides titled "Design Patterns in Dynamic Programming". His page for them went online on 17 March 1998. One slide carries the figure that is usually repeated: "16 of 23 patterns are either invisible or simpler".

The scope is narrower than the repetition. The comparison is between Dylan or Lisp and C++, and another slide states the finding in full: "16 of 23 patterns have qualitatively simpler implementation in Lisp or Dylan than in C++ for at least some uses of each pattern". Norvig attributes this to first-class types, first-class functions, macros and other features. He does not say that 16 patterns are unnecessary in dynamic languages.

Patterns as a sign of what a language lacks

From those slides others have drawn a stronger claim: that a pattern marks a feature missing from a language. That is an argument, not a finding.

The example adds a count of its own, not comparable with Norvig's. Eleven of the 23 files hold a shorter form made of a C# feature: delegates, lambda expressions, iterators, events, records, switch expressions. In each the code shrank and the problem stayed: somebody still chooses the shipping rule and decides how deep a copy goes. The twelve without one mostly describe an arrangement of responsibilities: Facade, Mediator, Composite, Adapter.

In our reading, a language can absorb a pattern's shape, as C# absorbed Iterator and Observer. It cannot absorb the problem, and the name stays useful while people need to talk about the problem.

The authors' second thoughts

In an interview published on 22 October 2009, Larry O'Brien spoke with Gamma, Helm and Johnson, and Gamma described a session in 2005 in which the authors reconsidered the list. Interpreter and Flyweight would have moved to a separate category. Factory Method would have been generalised to Factory. The candidates for addition included Null Object, Type Object, Dependency Injection and Extension Object/Interface. On one pattern he was direct: "I'm in favor of dropping Singleton. Its use is almost always a design smell."

The interview presents these as draft notes, not a revision, and no second edition followed.

Misuse

The remaining criticism is of readers, and it is our judgement, not history. The catalogue is classified by purpose, that is, by problem. The common misuse starts from the solution: class names that announce a pattern where nothing varies, an interface with one implementation and no test double, a review that counts patterns as evidence of design. Each pays for an indirection and collects nothing.

The opposite failure is quieter: the duplicated switch, the approval rule copied into three services. There the problem is present and the name would have helped.

Telling the two apart needs a criterion that the patterns do not supply. Part 5 takes the indirection these patterns share and examines the principles that judge it: coupling, cohesion, SOLID and dependency inversion. For the modern C# forms compared one by one, go to Part 8.

What to take from this

  • Before applying a pattern, say in one sentence what varies and who must not know about it. If nothing varies yet, write the plain code.
  • Use a pattern's name only for the problem it names.
  • When the varying thing is one method with no state, pass a delegate. Move to an interface when it gains a second member, a name, state, or a registration in a container.
  • Do not write a class that guards its own single instance. Register one instance in the composition root and inject it.
  • For a closed set of types and a growing set of operations, choose between a visitor and a switch expression by who must catch a missing case: the compiler or a test.

Run the code

The example is one download, software-foundations-example.zip: unpack it and run the commands in its foundations folder. It was built and its tests were run with the .NET 10 SDK on 13 November 2026, and it needs that SDK. .NET 10 is a long-term support release; .NET 11 was released on 10 November 2026 and the example does not need it.

  • dotnet test runs the tests.
  • dotnet run --project src/Pricing.Host prints the output shown above.

The 23 patterns are in src/Patterns, one file each. They are covered by CreationalPatternTests.cs, StructuralPatternTests.cs and BehaviouralPatternTests.cs in tests/Pricing.Tests: at least one test of behaviour for each pattern and, for each of the eleven with a -modern region, a test that the two forms give the same result. The Strategy of the pricing stages is covered by StrategyTests.cs, where The_interface_form_and_the_delegate_form_give_the_same_result runs both forms of Stage 06 against the table of orders.

References

Tags#software-foundations#software-design#engineering#dotnet#development#havecode

The Foundations of Software Engineering

From the origins of programming to modern architecture. 8 parts, read in order.

  1. 01
    Before Design Patterns: How Software Engineering Learned to Manage Complexity

    From Lovelace's Notes of 1843 to Brooks's essay of 1986, each advance in programming removed a difficulty of the tools and left the difficulty of the problem. One pricing routine, written three ways with identical output, shows what structure and names buy and what they do not.

  2. 02
    Structured Programming, Modularity and Information Hiding

    Structured control flow, procedures, modules, information hiding, coupling, cohesion and separation of concerns are separate ideas from separate authors. The same pricing rules, cut as functions and then as modules, show what Parnas's criterion localises and what it does not.

  3. 03
    Object-Oriented Programming: Origins, Promises and Trade-offs

    Object orientation is several mechanisms with separate origins: Simula's subclasses, abstract data types, Smalltalk's messaging. One pricing example shows what encapsulation and an interface buy, what they cost, and when functions over plain data stay the better design.

  4. 04
    The Gang of Four: 23 Design Patterns and the Problems They Solve

    You are here.

  5. 05
    SOLID, Coupling, Cohesion and Dependency Inversion

    Coupling and cohesion are the criteria; the five SOLID principles are later heuristics with separate origins in Meyer, Liskov and Martin. One pricing calculator shows what each buys and costs, and why dependency inversion, dependency injection and a container are three different things.

  6. 06
    Refactoring: Improving Design Without Changing Behaviour

    Refactoring changes structure against a recorded definition of behaviour, in steps small enough to check. One legacy pricing method is pinned by characterisation tests, reshaped in seven steps, and the tests themselves are proved able to fail.

  7. 07
    From Design Patterns to Domain-Driven Design and Modern Architecture

    Domain-driven design, ports and adapters, the onion and the clean architecture share one rule: the business rules refer to nothing else. The pricing example gains a domain model and a tested boundary, and needs 22 types where one class did.

  8. 08
    Do We Still Need Gang of Four Patterns in Modern C# and .NET?

    All 23 patterns set against C# 14 and .NET 10: eleven have a shorter form in the example, built from a language or platform feature, and twelve do not. The problems remain in every case, and each modern form has a failure of its own.

Support independent engineering knowledge

Knowledge, Analysis, the Field Guide and the tools are free to read and use, and they stay that way. If this article saved you time, you can support the research and writing behind it. Support is voluntary and unlocks nothing.

Buy me a coffee (opens in a new tab)João Lívio's page on buymeacoffee.com

Comments