Software Engineering
Design Patterns: GoF
1994. Gamma, Helm, Johnson, Vlissides - the Gang of Four. Design Patterns. 23 patterns. One of the best-selling programming books in history. Thirty years later, half of them are built into standard libraries. PyTorch nn.Module is the Composite pattern. Each neural network layer is a leaf node. Sequential is a composite node. model.parameters() traverses the entire tree through a uniform interface. A pattern from 1994 in 2024 production code.
- **PyTorch:** nn.Module (Composite) + autograd hooks (Observer) + optim (Strategy) - three GoF patterns in one framework
- **Hugging Face transformers:** AutoModel (Abstract Factory) + Pipeline (Facade) + Trainer callbacks (Observer)
- **FastAPI:** Middleware (Decorator chain) + Dependency Injection (Strategy/DIP variant) + Router (Composite)
- **LangChain:** Chain (Composite) + Tool (Command) + Memory (Strategy) + BaseLLM (Adapter)
Creational Patterns
1994. Gamma, Helm, Johnson, Vlissides - the Gang of Four. Design Patterns. 23 patterns. One of the best-selling programming books in history. Thirty years later, half of them are built into standard libraries. Creational patterns solve one problem: how to create objects without coupling to concrete classes. **Factory Method** - a method creates an object, but the subclass decides which one. **Abstract Factory** - a family of related objects without specifying concrete classes. **Builder** - step-by-step construction of a complex object, separating construction from representation. **Singleton** - a single instance with a global access point - and the most criticized GoF pattern.
**Singleton is the most dangerous GoF pattern.** It is global state wearing OOP clothes. Unit tests break: no way to isolate a component from a global singleton. Concurrency breaks: race conditions without explicit locking. The GoF book itself describes Singleton as 'not the best design.' The modern alternative is Dependency Injection - pass the dependency explicitly through the constructor instead of `getInstance()`.
Hugging Face `AutoModel.from_pretrained('bert-base-uncased')` returns a `BertModel`, while `AutoModel.from_pretrained('gpt2')` returns a `GPT2Model`. Which GoF pattern is this?
Structural Patterns
Structural patterns handle how classes and objects are composed into larger structures. **Adapter** - makes incompatible interfaces work together through a wrapper. **Decorator** - adds behavior dynamically without subclassing. **Composite** - a tree of objects with a uniform interface for leaves and nodes. **Facade** - a simplified interface over a complex subsystem. PyTorch `nn.Module` is the Composite pattern: each network layer is a leaf node, `nn.Sequential` is a composite node, `model.parameters()` traverses the entire tree through one interface. A pattern from 1994 in 2024 production code.
**Composite in neural networks:** the idea - one interface for atomic and composite elements - maps perfectly onto layer hierarchies. A single `Linear` layer and a 1,000-layer model have the same `forward()` and `parameters()` interface. Training code never needs special cases for depth. Arbitrary architecture composition works for free.
In PyTorch, `nn.Sequential(nn.Linear(128, 64), nn.ReLU(), nn.Dropout(0.3))` can be nested inside another `nn.Sequential`. `model.parameters()` recursively returns parameters from all levels. Which GoF pattern is this?
Behavioral Patterns
Behavioral patterns describe how objects interact and responsibilities are distributed. **Observer** - objects subscribe to events and receive notifications when state changes. **Strategy** - a family of interchangeable algorithms behind a common interface. **Command** - encapsulates a request as an object, enabling deferred and undoable operations. **Iterator** - sequential access to elements without exposing internal structure. A paradox: Strategy in Python is just passing a function as an argument. Java 8 introduced lambdas in 2014. Half the GoF patterns became unnecessary because Python, JS, and Kotlin supported first-class functions from the start.
**Python generators make Iterator trivial.** In Java, Iterator is a separate class with `hasNext()` and `next()`. In Python - `yield` in a function, with built-in `for x in obj:` support. The pattern did not disappear - it was absorbed into the language. DataLoader, file iterators, pandas iterrows - all Iterator under a different name.
Hugging Face Trainer accepts a list of `callbacks`: EarlyStoppingCallback, WandBCallback, LRSchedulerCallback. Each receives notifications about training events. Which GoF pattern?
When to Apply and When Not To
Christopher Alexander, author of the original pattern language idea: 'A pattern is a solution to a problem in a context.' Not a universal rule - a solution to a specific problem in a specific context. The main anti-pattern: applying GoF patterns because 'it is the right thing to do,' not because there is a real problem. Result: FactoryManagerFactoryBuilder to create one object. YAGNI (You Aren't Gonna Need It): do not add abstraction until there is a second implementation. First time - write directly. Second time - refactor. In ML systems, patterns emerge naturally: Strategy for model selection, Observer for training callbacks, Command for job queues. No need to force them.
**Patterns as vocabulary, not as a goal.** GoF patterns are a shared language for teams: saying 'Observer for training events' is faster than explaining the architecture from scratch. The goal is not 'add more patterns' - it is solving a specific problem with the least necessary complexity. If a problem is solved by direct code, direct code is better.
More GoF patterns in a codebase means better architecture
Patterns are a vocabulary for describing solutions, not a goal in themselves. Good architecture solves a problem with the minimum necessary complexity.
Over-engineering with patterns is a real production problem. FactoryManagerFactoryBuilder exists in real codebases. A pattern solves a specific problem in a specific context. No problem - no pattern. Functional style in Python, JS, and Kotlin often replaces entire GoF class hierarchies with a single first-class function.
An ML engineer adds a second LLM provider (Anthropic) to existing code that only worked with OpenAI. When is the right moment to introduce the Strategy pattern?
Key Ideas
- **Creational** (Factory, Builder, Singleton): controlling object creation. Singleton is the most famous and most dangerous - global state wearing OOP clothes
- **Structural** (Adapter, Decorator, Composite, Facade): composing objects. nn.Module in PyTorch is Composite in action
- **Behavioral** (Observer, Strategy, Command, Iterator): distributing responsibilities. In Python many of these reduce to a callable or a generator
- **When to apply:** at the second implementation (Strategy), when notifications are needed (Observer), when a tree of objects exists (Composite). Not earlier - YAGNI
Related Topics
GoF patterns appear across the entire development stack:
- SOLID Principles — GoF patterns are concrete SOLID implementations. Strategy implements OCP, Dependency Injection implements DIP.
- Clean Architecture — GoF patterns as building blocks: Repository (Adapter + Strategy), Use Cases (Command)
- AI Engineering: API Integration — LLM providers through Adapter, request parameters through Builder, model selection through Strategy
- Programming Language Theory — First-class functions and lambdas in modern languages replace half of GoF patterns
- Strings and Collections — Python generators are the Iterator pattern built in. for x in obj: is GoF Iterator as syntactic sugar
Вопросы для размышления
- PyTorch Trainer accepts a list of callbacks (EarlyStopping, ModelCheckpoint, LRScheduler). Which GoF pattern does this implement? Why is it better than if/else inside the training loop?
- In Python the Strategy pattern is often replaced by passing a function as an argument. When is it worth creating a full Strategy class instead of a callable?
- Singleton is considered an anti-pattern in testable code. What real use cases justify it and how can the risks be minimized?