Design patterns
- Reading time
- 5 min read
- Word count
- 903 words
- Diagram count
- 0 diagrams
Source: Victor Bona's Obsidian Compendium snapshot, Knowledge base/Design Patterns/Design patterns.md.
Design patterns
- Adapter Pattern
- Bridge Pattern
- Builder Pattern
- Chain of Responsibility Pattern
- Command Pattern
- Composite Pattern
- CQRS (Command Query Responsibility Segregation)
- Decorator Pattern
- Dependency Injection (and Inversion of Control)
- Event Sourcing
- Facade Pattern
- Factory Method and Abstract Factory
- Microkernel (Plugin) Architecture
- Microservices Architecture
- Model-View-Controller (MVC)
- Observer Pattern
- Prototype Pattern
- Proxy Pattern
- Repository Pattern
- Singleton
- Strategy Pattern
- Template Method Pattern
- Outbox Pattern
- Saga Pattern for Distributed Transactions
Patterns in Functional Paradigms
Many of the classic design patterns emerged from object-oriented programming. In functional programming (FP), some of these patterns either become simpler or unnecessary due to language features like first-class functions, immutability, and powerful type systems. It's valuable for a well-rounded engineer to understand how similar problems are approached in FP:
-
First-class and Higher-Order Functions: In FP, you can pass functions as arguments, return them from other functions, and store them in variables. This capability often replaces patterns like Strategy or Command. For example, instead of having a
DiscountStrategyinterface with multiple classes, you could have a dictionary of functions (or just pass a different function to apply discount). The effect is the same: behavior is parameterized, but with less boilerplate. Example: In JavaScript/TypeScript, one might do:const strategies = { noDiscount: amount => amount, tenPercent: amount => amount * 0.9, flat5: amount => Math.max(0, amount - 5) }; // Use a strategy: let total = strategies.tenPercent(100);This is a functional take on the Strategy pattern – no classes or interfaces needed, just functions.
-
Functions as Observers: The Observer pattern can be seen in FP via callback functions or reactive streams. For instance, instead of an object implementing an
updatemethod, you might register a function to an event stream. Many modern JavaScript implementations use this (e.g.,Array.forEach(callback)or Node’s EventEmitter where youon('event', callback)). These are essentially observer patterns expressed as higher-order functions. -
Immutability and Pure Functions: Patterns like Memento (capturing state for undo) become trivial if you have immutable data structures – you can just keep the old copy as the memento. Similarly, Command in FP might just be representing an operation as a function or closure, which naturally can be stored or passed around (for undo, one might store the inverse function too).
-
No need for Singleton: In FP, if something is meant to be a single instance, you can just create it as a module or a constant. Since pure functions have no state, the concept of limiting instances is less relevant except for external resources (which can be handled by scope).
-
Monads and other FP patterns: Functional programming has its own idioms which some call "design patterns of FP." For example, a Monad can be viewed as a design pattern for wrapping and sequencing computations (for handling things like null/undefined (
Maybemonad instead of Null Object or checks), or asynchronous computations (Promisemonad), etc.). These abstract a common flow (compute, and if missing value then propagate a none). While monads come from category theory, practically they solve certain recurring problems (like chaining operations that might fail without deeply nested conditionals). -
Pattern Matching vs. Visitor: In languages with algebraic data types (ADTs) and pattern matching (like Haskell or Scala), adding new operations to a structure is often done with pattern matching on variants, which can be more straightforward than the Visitor pattern in OO. The Visitor is essentially a workaround for the lack of multiple dispatch in OO languages; FP languages often allow you to simply write functions that deconstruct variants.
-
Recursion and higher-order operations vs. Iterator: Instead of using an Iterator object to traverse a collection (as in OO iterator pattern), FP favors higher-order functions like map, filter, reduce, or recursion. These abstract the iteration pattern so explicitly needing an iterator is rare.
Table: OOP Pattern vs FP Approach (a few examples for clarity):
-
Strategy (OOP): define interface + classes for each algorithm. Strategy (FP): pass function as parameter or choose function from a map/record.
-
Command (OOP): encapsulate operation in an object with execute(). Command (FP): use a closure or function that captures the needed context (for undo, maybe return a function that performs the inverse).
-
Observer (OOP): objects register as listeners, subject calls update on them. Observer (FP): callbacks registered, or use reactive stream (observables) that functions subscribe to.
-
Template Method (OOP): abstract class with method using self-calls to hooks. FP approach: higher-order function that takes necessary operation(s) as parameters. (E.g., instead of a base class calling a subclass method, you write a function that accepts another function to fill in the blank.)
-
Singleton (OOP): private constructor, global access. Singleton (FP): Just a module or a single immutable value (since there's no stateful object creation in the same sense, it's simply not a needed construct).
In summary, functional programming often replaces object-oriented patterns with language constructs. Many patterns are about achieving polymorphism, extensibility, or controlling side effects – FP provides alternate mechanisms (first-class functions, immutability, pure functions, and powerful type systems) to address these. However, the underlying problems (like modularizing behavior, handling state, etc.) still exist, so one could say FP has its own patterns (even if they aren't named like GoF).
For a back-end engineer, it’s useful to realize that if you switch to or work with functional languages, you might use fewer classical patterns explicitly, but you will use functional idioms that serve similar purposes. Knowing both perspectives allows choosing the best approach in a multi-paradigm environment (for instance, using a functional style to simplify the implementation of a strategy or command within an otherwise OO codebase).