GoF Design Patterns

12 minute read

(AI summarized reading)

Published:

Design Patterns: Elements of Reusable Object-Oriented Software

Authors: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (the “Gang of Four”) Published: 1994 Source: https://www.javier8a.com/itc/bd1/articulo.pdf


Overview

A foundational text in software engineering that catalogs 23 recurring solutions to common object-oriented design problems. The book argues that experienced designers don’t solve problems from scratch — they reuse patterns that have worked before. A design pattern names, abstracts, and identifies the key aspects of a common design structure that makes it useful for creating reusable OO design.


Core Concepts

What is a Design Pattern?

Each pattern has four essential elements:

  1. Pattern name — a handle to describe a design problem and its solution in one or two words
  2. Problem — describes when to apply the pattern and its context
  3. Solution — describes the elements, relationships, responsibilities, and collaborations
  4. Consequences — trade-offs and results of applying the pattern (space, time, flexibility)

Key OO Design Principles

  • Program to an interface, not an implementation — depend on abstract types, not concrete classes
  • Favor object composition over class inheritance — keeps each class encapsulated and focused on one task
  • Find what varies and encapsulate it — identifies what should be hidden behind an abstraction

How Patterns Relate to Design

  • Patterns help you design flexible, reusable OO software
  • They capture solutions that have been developed and evolved over time
  • Knowing patterns makes it easier to communicate design decisions

The 23 Patterns

Organized into three categories based on purpose:

🏗️ Creational Patterns

Deal with object creation mechanisms, decoupling the system from how its objects are created.

Abstract Factory

  • Intent: Provide an interface for creating families of related or dependent objects without specifying their concrete classes
  • Use when: A system must be independent of how its products are created; you want to enforce constraints among related products
  • Key idea: Factory methods grouped into a factory object; swap the whole factory to change a family of products
  • Example: UI toolkit that must work on multiple platforms (Windows / macOS widgets)

Builder

  • Intent: Separate the construction of a complex object from its representation so the same construction process can create different representations
  • Use when: The algorithm for creating a complex object should be independent of the parts that make up the object
  • Key idea: A Director orchestrates a Builder; the Builder assembles the product step by step
  • Example: Document converters (RTF → PDF, RTF → TeX), meal builders

Factory Method

  • Intent: Define an interface for creating an object, but let subclasses decide which class to instantiate. Lets a class defer instantiation to subclasses
  • Use when: A class can’t anticipate the class of objects it must create; subclasses should specify the objects they create
  • Key idea: “Virtual constructor” — override a creator method to return a different product
  • Example: Application.createDocument() deferred to DrawingApplication, SpreadsheetApplication

Prototype

  • Intent: Specify the kinds of objects to create using a prototypical instance, and create new objects by copying (cloning) this prototype
  • Use when: Classes to instantiate are specified at run-time; the cost of creating a new object via new is high
  • Key idea: Clone an existing object instead of constructing from scratch
  • Example: Graphical editors where users copy-paste shapes

Singleton

  • Intent: Ensure a class only has one instance, and provide a global point of access to it
  • Use when: Exactly one object is needed to coordinate actions across the system
  • Key idea: Hide the constructor; expose a static getInstance() method
  • Caution: Overuse leads to tight coupling and difficulty testing; prefer dependency injection where possible
  • Example: Logger, configuration manager, thread pool

🔧 Structural Patterns

Deal with how classes and objects are composed to form larger structures.

Adapter

  • Intent: Convert the interface of a class into another interface clients expect. Lets incompatible interfaces work together
  • Use when: You want to use an existing class but its interface doesn’t match what you need
  • Two forms: Class adapter (multiple inheritance) and Object adapter (composition)
  • Example: Adapting a third-party graphics library’s LegacyRectangle to your Shape interface

Bridge

  • Intent: Decouple an abstraction from its implementation so that the two can vary independently
  • Use when: You want to avoid a permanent binding between abstraction and implementation; both should be extensible via subclassing
  • Key idea: The “abstraction” holds a reference to an “implementor” rather than inheriting from one
  • Example: Window system (Window abstraction) separated from platform implementation (XWindow, PMWindow)

Composite

  • Intent: Compose objects into tree structures to represent part-whole hierarchies. Lets clients treat individual objects and compositions uniformly
  • Use when: You want clients to be able to ignore the difference between compositions of objects and individual objects
  • Key idea: Leaf and Composite share the same Component interface
  • Example: File system (files and directories), UI widget trees, document structure (paragraphs inside sections inside chapters)

Decorator

  • Intent: Attach additional responsibilities to an object dynamically. Provides a flexible alternative to subclassing for extending functionality
  • Use when: You want to add responsibilities without subclassing; responsibilities can be withdrawn; extension by subclassing is impractical due to a large number of combinations
  • Key idea: Wrap the component in a decorator that delegates to it and adds behavior before/after
  • Example: Streams with scrollbars and borders; Java’s BufferedInputStream

Facade

  • Intent: Provide a unified interface to a set of interfaces in a subsystem. Defines a higher-level interface that makes the subsystem easier to use
  • Use when: You want to provide a simple interface to a complex subsystem; there are many dependencies between clients and implementation classes
  • Key idea: Wrap a complex API in a simpler one; doesn’t prevent access to the underlying system
  • Example: Compiler façade (Compiler.compile()) hiding Scanner, Parser, ProgramNode, etc.

Flyweight

  • Intent: Use sharing to support large numbers of fine-grained objects efficiently
  • Use when: An application uses a large number of objects that have much state in common; storage costs are high due to quantity
  • Key idea: Split state into intrinsic (shared, stored in flyweight) and extrinsic (context-dependent, passed by client)
  • Example: Characters in a text editor, particles in a game engine

Proxy

  • Intent: Provide a surrogate or placeholder for another object to control access to it
  • Use when: You need a smarter or more versatile reference to an object than a plain pointer
  • Types: Remote proxy, virtual proxy (lazy loading), protection proxy, smart reference
  • Example: Image proxy that loads the full image only when draw() is called

🔄 Behavioral Patterns

Deal with communication, responsibility, and algorithms between objects.

Chain of Responsibility

  • Intent: Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle it. Chain the receiving objects and pass the request along until one handles it
  • Use when: More than one object may handle a request; the handler isn’t known a priori
  • Example: Help system, event handling in UI frameworks, middleware pipelines

Command

  • Intent: Encapsulate a request as an object, letting you parameterize clients with different requests, queue or log requests, and support undoable operations
  • Use when: You need to parameterize actions, support undo, implement transactions
  • Key idea: Command object has an execute() method; an Invoker stores and calls commands
  • Example: Menu items, macro recording, job queues, transactional systems

Interpreter

  • Intent: Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language
  • Use when: The grammar is simple and efficiency is not critical
  • Example: Regular expressions, SQL parsing, scripting languages

Iterator

  • Intent: Provide a way to sequentially access elements of an aggregate object without exposing its underlying representation
  • Use when: You need a standard way to traverse different types of collections
  • Key idea: Separates traversal logic from the collection; supports multiple concurrent traversals
  • Example: Java’s Iterator, C++’s STL iterators

Mediator

  • Intent: Define an object that encapsulates how a set of objects interact. Promotes loose coupling by keeping objects from referring to each other explicitly
  • Use when: A set of objects communicate in well-defined but complex ways; reusing objects is difficult because they refer to many others
  • Example: Chat room (all messages routed through a central server), UI dialog boxes coordinating widget interactions

Memento

  • Intent: Without violating encapsulation, capture and externalize an object’s internal state so the object can be restored to this state later
  • Use when: A snapshot of an object’s state must be saved so it can be restored later (undo)
  • Key idea: A Caretaker stores Memento objects; only the Originator can access the memento’s internals
  • Example: Undo in text editors, save-states in games

Observer

  • Intent: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically
  • Use when: A change in one object requires changing others, and you don’t know how many objects need to change
  • Key idea: Subject maintains a list of observers and notifies them on state change
  • Example: MVC (Model notifies Views), event listeners, publish-subscribe systems

State

  • Intent: Allow an object to alter its behavior when its internal state changes. The object will appear to change its class
  • Use when: An object’s behavior depends on its state and must change at run-time; operations have large multipart conditionals on state
  • Key idea: Delegate state-specific behavior to State objects; transition by replacing the current state
  • Example: TCP connection states (Established, Listen, Closed), vending machine, order processing workflow

Strategy

  • Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable. Lets the algorithm vary independently from clients that use it
  • Use when: Many related classes differ only in behavior; you need different variants of an algorithm
  • Key idea: Extract the algorithm into a Strategy object; the Context delegates to it
  • Example: Sorting algorithms, compression strategies, payment methods, text layout

Template Method

  • Intent: Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Lets subclasses redefine certain steps without changing the algorithm’s structure
  • Use when: You want to implement the invariant parts of an algorithm once and let subclasses fill in the variable behavior
  • Key idea: Inversion of control (“the Hollywood Principle: don’t call us, we’ll call you”)
  • Example: Application framework lifecycle hooks, data parsing pipelines with fixed steps

Visitor

  • Intent: Represent an operation to be performed on the elements of an object structure. Lets you define a new operation without changing the classes of the elements on which it operates
  • Use when: An object structure contains many classes with differing interfaces, and you want to perform operations that depend on their concrete classes
  • Key idea: Double dispatch — the element accepts a visitor, which then dispatches on its own type
  • Tradeoff: Adding new element types is hard (requires updating all visitors); adding new operations is easy
  • Example: AST traversals in compilers, document rendering in different formats

Pattern Relationships & Cheat Sheet

PatternCategoryCore Idea
Abstract FactoryCreationalFamily of related objects
BuilderCreationalComplex object step-by-step
Factory MethodCreationalSubclass decides what to create
PrototypeCreationalClone existing instance
SingletonCreationalOne instance, global access
AdapterStructuralInterface translation
BridgeStructuralAbstraction ↔ Implementation split
CompositeStructuralTree of objects, uniform interface
DecoratorStructuralAdd behavior by wrapping
FacadeStructuralSimple interface to complex subsystem
FlyweightStructuralShare fine-grained objects
ProxyStructuralControlled object access
Chain of ResponsibilityBehavioralPass request along a chain
CommandBehavioralEncapsulate request as object
InterpreterBehavioralLanguage grammar as objects
IteratorBehavioralTraverse collection uniformly
MediatorBehavioralCentral communication hub
MementoBehavioralSnapshot for undo
ObserverBehavioralNotify dependents on change
StateBehavioralBehavior changes with state
StrategyBehavioralSwappable algorithms
Template MethodBehavioralSkeleton algorithm, subclass fills in
VisitorBehavioralAdd operations without modifying classes

Key Takeaways & Notes

  • Inheritance vs. Composition: The book strongly advocates for composition over inheritance. Inheritance is a white-box relationship (exposes parent internals); composition is black-box (only the interface is visible). Prefer it for flexibility.
  • Interfaces matter more than implementations: Design to abstractions. If you depend on concrete classes, you’re coupled to their implementations.
  • Patterns work together: Many patterns collaborate naturally (e.g., Abstract Factory + Singleton; Composite + Iterator + Visitor; Strategy + Template Method).
  • Don’t over-pattern: Patterns add indirection and complexity. Only apply them when flexibility or reuse justifies it.
  • Common design challenges patterns address:
    • Finding appropriate objects
    • Determining object granularity
    • Specifying object interfaces
    • Specifying object implementations
    • Designing for change

Personal Notes