Complete Guide | Lesson 7: SOLID → Lesson 8: Design Patterns → Lesson 9: Generics (May 21)
Every problem you'll ever face in software has already been solved. The question is whether you'll spend six months reinventing the wheel or six minutes reading about the solution someone catalogued thirty years ago.
This is Lesson 8, and it maps to Tiphareth on the Tree of Life. Tiphareth means "Beauty" or "Harmony." It sits at the exact center of the Tree, balancing all the forces above and below. Design patterns are the harmonizing force of software: proven solutions that balance flexibility with structure, abstraction with implementation, cleverness with clarity. They're beautiful not because they're complex, but because they're precisely the right amount of architecture for the problem at hand.
What Are Design Patterns?
In 1994, four authors (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, collectively known as the Gang of Four or GoF) published Design Patterns: Elements of Reusable Object-Oriented Software. It catalogued 23 patterns: recurring solutions to recurring problems. The book didn't invent these patterns. It named them. And naming them changed everything.
Before the book, two developers might independently solve the same problem with the same structural approach and not realize they were doing the same thing. After the book, you could say "use the Observer pattern" and every developer in the room understood the entire architecture in two words.
The 23 patterns fall into three categories:
Creational patterns: How objects are born. Factory, Builder, Singleton, Prototype, Abstract Factory.
Structural patterns: How objects are organized. Adapter, Decorator, Facade, Composite, Bridge, Proxy, Flyweight.
Behavioral patterns: How objects communicate. Observer, Strategy, Command, State, Template Method, Iterator, Mediator, Chain of Responsibility, Visitor, Memento, Interpreter.
You don't need to memorize all 23. You need to deeply understand about 6 of them and recognize the rest when you encounter them. Here are the ones that matter most.
Creational Patterns: How Things Are Born
Factory Method
Instead of calling new SomeClass() directly, you ask a factory to create the right object. The factory decides which concrete class to instantiate based on the input.
Analogy: you walk into a restaurant and order "a burger." You don't walk into the kitchen and assemble the ingredients yourself. The kitchen (factory) handles the creation. You just specify what you want.
1CLASS ShapeFactory
2 STATIC METHOD create(type: String): Shape
3 IF type == "circle" THEN
4 RETURN new Circle()
5 ELSE IF type == "rectangle" THEN
6 RETURN new Rectangle()
7 ELSE IF type == "triangle" THEN
8 RETURN new Triangle()
9 END IF
10 END METHOD
11END CLASS
12
13// Usage: the caller never uses "new" directly
14shape = ShapeFactory.create("circle")Why this matters: the caller doesn't need to know which concrete class is created. If you add a Hexagon class later, you update the factory. The calling code doesn't change.
Builder
Construct complex objects step by step instead of passing 15 parameters to a constructor.
Analogy: building a custom PC. You choose the CPU, then the RAM, then the GPU, then the storage. Each step is independent. The builder assembles the final product.
1pizza = PizzaBuilder()
2 .setSize("large")
3 .setCrust("thin")
4 .addTopping("pepperoni")
5 .addTopping("mushrooms")
6 .addSauce("marinara")
7 .build()Without Builder, you'd have: new Pizza("large", "thin", ["pepperoni", "mushrooms"], "marinara", null, null, false, true). What's the seventh parameter? Nobody knows. Builder makes the construction self-documenting.
Singleton (The Controversial One)
Ensure only one instance of a class exists, ever. Like a president: only one at a time, globally accessible.
1CLASS DatabaseConnection
2 PRIVATE STATIC _instance: DatabaseConnection = null
3
4 PRIVATE CONSTRUCTOR()
5 // Private! Nobody can call "new DatabaseConnection()"
6 END CONSTRUCTOR
7
8 STATIC METHOD getInstance()
9 IF _instance == null THEN
10 _instance = new DatabaseConnection()
11 END IF
12 RETURN _instance
13 END METHOD
14END CLASS
15
16// Everyone gets the same connection:
17db1 = DatabaseConnection.getInstance()
18db2 = DatabaseConnection.getInstance()
19// db1 and db2 are the same objectNow here's the controversy: Singleton is widely considered an antipattern. It's global state in disguise. It creates hidden dependencies (any code can access it from anywhere). It makes testing nearly impossible (you can't inject a mock). It violates the Single Responsibility Principle (the class manages both its behavior AND its own lifecycle).
The Singleton Problem
Singleton is the goto of object-oriented programming: technically legal, occasionally useful, and almost always a sign that something better exists. When you think you need a Singleton, you usually need dependency injection instead. Pass the dependency in through the constructor (Lesson 6). Let the caller decide what instance to use. Your code becomes testable, your dependencies become explicit, and you don't have global state hiding in corners.
When Singleton is acceptable: logging (maybe), configuration loading at startup (maybe), connection pools (maybe). The word "maybe" appears three times because the answer is almost always "use dependency injection instead."
Structural Patterns: How Things Are Organized
Adapter
Makes incompatible interfaces work together. Like a travel power adapter: your American plug doesn't fit a European outlet, so you use an adapter in between.
1// Old payment system with incompatible interface
2CLASS OldPaymentGateway
3 METHOD makePayment(dollars, cents, cardNum)
4 END METHOD
5END CLASS
6
7// Your system expects IPayable (from Lesson 6)
8CLASS PaymentAdapter IMPLEMENTS IPayable
9 gateway: OldPaymentGateway
10
11 METHOD processPayment(amount: Decimal)
12 dollars = floor(amount)
13 cents = round((amount - dollars) * 100)
14 this.gateway.makePayment(dollars, cents, this.cardNum)
15 END METHOD
16END CLASSThe adapter translates between what your code expects and what the old system provides. Neither side needs to change.
Decorator
Adds behavior to an object without changing its class. Like wrapping a gift: the gift stays the same, but now it has wrapping paper, a bow, and a card.
1// Base coffee
2coffee = new SimpleCoffee() // $2.00
3
4// Decorate it
5coffee = new MilkDecorator(coffee) // $2.50
6coffee = new SugarDecorator(coffee) // $2.60
7coffee = new WhipCreamDecorator(coffee) // $3.10
8
9// Each decorator wraps the previous one, adding cost and description
10coffee.getCost() // $3.10
11coffee.getDescription() // "Simple coffee, milk, sugar, whip cream"Each decorator adds functionality while maintaining the same interface. You can stack them in any order, any combination.
Facade
Provides a simple interface to a complex subsystem. Like a TV remote: one button turns on the TV, receiver, speakers, and streaming box. You don't configure each device separately.
1CLASS HomeTheaterFacade
2 tv: Television
3 receiver: AudioReceiver
4 speakers: SurroundSound
5 streaming: StreamingBox
6
7 METHOD watchMovie(title)
8 this.tv.turnOn()
9 this.receiver.turnOn()
10 this.speakers.setVolume(40)
11 this.streaming.launch()
12 this.streaming.play(title)
13 END METHOD
14
15 METHOD endMovie()
16 this.streaming.stop()
17 this.speakers.setVolume(0)
18 this.receiver.turnOff()
19 this.tv.turnOff()
20 END METHOD
21END CLASS
22
23// One call instead of five:
24theater.watchMovie("Inception")Behavioral Patterns: How Things Communicate
Observer
When one object changes state, all subscribers are notified automatically. Like subscribing to a YouTube channel: the creator publishes, all subscribers get the update.
1CLASS EventEmitter
2 subscribers: Map<String, List<Function>>
3
4 METHOD subscribe(event, callback)
5 this.subscribers[event].add(callback)
6 END METHOD
7
8 METHOD emit(event, data)
9 FOR EACH callback IN this.subscribers[event]
10 callback(data)
11 END FOR
12 END METHOD
13END CLASS
14
15// Subscribe
16priceTracker.subscribe("priceChanged", updateDashboard)
17priceTracker.subscribe("priceChanged", sendAlert)
18priceTracker.subscribe("priceChanged", logToDatabase)
19
20// When price changes, ALL subscribers fire:
21priceTracker.emit("priceChanged", newPrice)The price tracker doesn't know what the subscribers do. It just notifies them. Each subscriber decides its own response. This is the foundation of every event-driven system, every notification service, and every reactive UI framework.
Strategy
Swap algorithms at runtime. Like choosing a route on GPS: fastest, shortest, or scenic. Same destination, different approach.
1INTERFACE ISortStrategy
2 METHOD sort(data: List): List
3END INTERFACE
4
5CLASS QuickSort IMPLEMENTS ISortStrategy
6 METHOD sort(data) // Fast for large datasets
7END CLASS
8
9CLASS InsertionSort IMPLEMENTS ISortStrategy
10 METHOD sort(data) // Fast for small or nearly-sorted datasets
11END CLASS
12
13CLASS Sorter
14 strategy: ISortStrategy
15
16 METHOD setStrategy(strategy)
17 this.strategy = strategy
18 END METHOD
19
20 METHOD sort(data)
21 RETURN this.strategy.sort(data)
22 END METHOD
23END CLASS
24
25// Swap algorithms at runtime:
26sorter = new Sorter()
27sorter.setStrategy(new QuickSort())
28sorter.sort(largeDataset)
29
30sorter.setStrategy(new InsertionSort())
31sorter.sort(smallDataset)Strategy lets you change behavior without changing the object that uses it. This is composition (Lesson 5) and interfaces (Lesson 6) working together.
Command
Encapsulate a request as an object. Like a restaurant order ticket: it can be queued, tracked, undone, or replayed.
1INTERFACE ICommand
2 METHOD execute()
3 METHOD undo()
4END INTERFACE
5
6CLASS AddToCartCommand IMPLEMENTS ICommand
7 cart: Cart
8 item: Item
9
10 METHOD execute()
11 this.cart.add(this.item)
12 END METHOD
13
14 METHOD undo()
15 this.cart.remove(this.item)
16 END METHOD
17END CLASS
18
19// Commands can be stored, queued, undone:
20history = []
21cmd = new AddToCartCommand(cart, shoes)
22cmd.execute()
23history.push(cmd)
24
25// Undo!
26lastCmd = history.pop()
27lastCmd.undo()Every undo/redo system, every transaction log, and every macro recorder uses the Command pattern.
When NOT to Use Patterns
This is the most important section of this lesson: pattern abuse is worse than no patterns at all.
The Golden Hammer: When you learn a new pattern, everything looks like a nail. You just learned Factory? Suddenly every object creation needs a factory, even when new Thing() works perfectly.
YAGNI: You Aren't Gonna Need It. Don't add a Strategy pattern when you have one algorithm. Don't create an Observer system for one listener. Don't build a Factory for one product.
Simple beats clever. A straightforward if/else that everyone understands is better than a pattern that only you understand. Code is read 10 times more often than it's written. Optimize for readability.
When you DO need a pattern: When you find yourself writing the same structural solution in multiple places. When you're fighting your own code to add features. When testing becomes impossible because of dependencies. That's when a pattern earns its place.
"Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away." Antoine de Saint-Exupery said that about design. It applies perfectly to pattern use: the right amount is the minimum that solves the problem.
Pattern Selection Cheat Sheet
Need to create objects without specifying exact classes? → Factory Need to build complex objects step by step? → Builder Need to add behavior without modifying existing classes? → Decorator Need to simplify a complex subsystem? → Facade Need to notify multiple objects of state changes? → Observer Need to swap algorithms at runtime? → Strategy Need undo/redo or command queuing? → Command Need to make incompatible interfaces work together? → Adapter Need to ensure only one instance? → Dependency injection first. Singleton only as a last resort.