Good design is what keeps software changeable as it grows. Revisit the OOP foundations — encapsulation, abstraction, inheritance, polymorphism — that every pattern builds on.
Why: software is read and changed far more than it is written, so the goal of design is not cleverness but changeability — code that new requirements can be added to without rewrites or ripples of breakage. When: every principle and pattern in this course exists to manage change and dependencies. Where: an architect judges a design by how well it absorbs the changes that are coming.
The one question good design answers:
"When the requirements change (they will), how much has to change with them?"
GOOD design a change touches ONE place; the rest is stable
BAD design a change ripples through many files; things break far away
Principles + patterns are TOOLS for localizing change and taming
dependencies. That's the whole game.Why: object-oriented design rests on four ideas — encapsulation (hide internal state behind a small interface), abstraction (model the essentials), inheritance (share behavior), and polymorphism (one interface, many implementations) — and every pattern is a disciplined use of them. When: reach for encapsulation and polymorphism constantly; inheritance far less than people expect. Where: polymorphism is the pillar most patterns lean on — it lets code depend on an interface, not a concrete type.
// Encapsulation: state is private, changed only through methods that keep it valid.
class BankAccount {
private balance = 0 // no one can set a negative balance directly
deposit(amount: number) {
if (amount <= 0) throw new Error('must be positive')
this.balance += amount
}
getBalance() {
return this.balance
}
}
// Polymorphism: callers depend on the shape, not the concrete class.
interface Shape {
area(): number
}
function totalArea(shapes: Shape[]) {
return shapes.reduce((sum, s) => sum + s.area(), 0) // works for ANY Shape
}Why: inheritance is the tightest coupling in OOP — a subclass depends on its parent’s internals and a deep hierarchy becomes rigid — so favor composition (build behavior by combining small objects) which stays flexible. When: use inheritance for genuine "is-a" relationships that are stable; use composition ("has-a") for almost everything else. Where: this preference underlies most design patterns, which compose objects rather than extend classes.
// RIGID (inheritance): behavior is locked into the class hierarchy.
// class FlyingDuck extends Duck {} // what about a RubberDuck that can't fly?
// FLEXIBLE (composition): plug behavior in, swap it at runtime.
interface FlyBehavior { fly(): string }
class CanFly implements FlyBehavior { fly() { return 'flying' } }
class NoFly implements FlyBehavior { fly() { return "can't fly" } }
class Duck {
constructor(private flyBehavior: FlyBehavior) {}
performFly() { return this.flyBehavior.fly() }
}
new Duck(new NoFly()) // a rubber duck — no hierarchy gymnastics needed