The four pillars
Abstraction, encapsulation, inheritance and polymorphism are the four ideas every object-oriented language is built to express. Design patterns are just these four applied under pressure, repeatedly, to specific recurring problems.
One real thing, several honest partial models. Keep the details your context needs and drop the rest.
A small public surface you promise to keep stable, and private internals you reserve the right to rewrite on a Tuesday.
A subclass gets the parent members for free and adds only what differs. The bill: it inherits the whole interface, useful parts and all.
Address the abstraction and let the runtime pick the implementation. Your code says Animal, the object knows it is a Cat.
Abstraction
Modelling only what your context cares about. An Airplane in a flight
simulator tracks thrust, fuel burn and control surfaces. An Airplane in a booking system
tracks a seat map. Same real object, two honest models, zero overlap. An abstraction that
tries to represent everything is just a worse copy of reality.
Same trick shows up outside travel apps. A Photo uploaded to a social feed models
caption, likeCount and a handful of filters. The same photo, opened in a print shop's
ordering tool, models dpi, colorProfile and paperSize. Neither model is wrong and
neither one needs the other's fields; the feed does not care what paper stock you print on,
and the print shop does not care how many people liked it.
Common mistake: building a leaky abstraction - a print-shop Photo.prepareForPrint()
that still forces the caller to check if (photo.sourceFormat === 'RAW') convertColorSpace(...)
before it can proceed. If the caller has to know the internals to use the interface
correctly, the abstraction bought you a new name, not a new model.
Try it yourself: design a Logger abstraction that has to work for both a CLI tool
printing to stdout and a production service shipping structured JSON to a log aggregator -
what single method earns a place on the interface, and what do you deliberately leave out?
Encapsulation
The interface a car gives you: a start button, a wheel, three pedals.
The crankshaft is real, and it is none of your business. In code that means a public surface
you promise to keep stable and private internals you reserve the right to rewrite on a
Tuesday. Take it further and you declare that surface as an actual interface: an Airport
that accepts anything implementing FlyingTransport will happily handle an airplane, a
helicopter, or a domesticated gryphon, because it only ever knew the method signatures.
Consider a NotificationCenter used by a mobile app. Its public surface is one method,
send(message). Behind that method it privately batches messages to avoid rate limits,
retries failed pushes with exponential backoff, and swaps providers when one goes down for
maintenance. None of that machinery is exposed, so the on-call engineer can swap the push
provider next month without touching a single call site.
Common mistake: encapsulating the field but not the behavior - marking every field
private and then adding a public getter and setter for each one. A getRetryCount() /
setRetryCount(n) pair on NotificationCenter hands the caller the same freedom to corrupt
state as a public field would, just with extra ceremony in between.
Try it yourself: NotificationCenter currently exposes send(message) and, out of
habit, a setRetryCount(n) setter too - decide whether that setter earns its place on the
public surface, and what you would offer instead if a caller genuinely needs to tune
retries.
Inheritance
Building a new class on an existing one, mostly to reuse code. The bargain has a price: a subclass inherits the full interface of its parent, and it cannot hand any of it back. You must implement every abstract method, including the ones that make no sense for you. One superclass per class, but as many interfaces as you like.
A reporting tool models Invoice and ExpenseReport as subclasses of an abstract
Document. Both inherit save(), print() and a createdAt timestamp for free, and each
adds its own method: Invoice gets computeTax(), ExpenseReport gets
flagOverBudget(). The reuse is real, but so is the bargain: both subclasses are now stuck
honoring every method Document declares, whether or not it makes sense for them.
Common mistake: reaching for inheritance purely to reuse code when there is no real
is-a relationship - making ExpenseReport extend Invoice just because both happen to have
a total field and a save() method, when neither is actually a kind of the other. The
fields matching is a coincidence, not a taxonomy.
Try it yourself: Document currently declares print() for every subclass - imagine a
DraftMemo subclass that is never meant to be printed, and decide whether it should inherit
print() anyway, override it to throw, or whether Document needs restructuring.
Polymorphism
The runtime figuring out what an object actually is. Declare
makeSound() abstract on Animal, put cats and dogs in a bag, iterate the bag as
Animal, and each one emits its own noise. Your code addressed the abstraction; the
implementations answered.
makeSound() on an Animal and the subclass answers. These are UML comments: notes about how a method is implemented.A drawing app keeps a list of Shape objects: some Circle, some Rectangle, some
Triangle. It never asks which is which. It just loops over the list and calls
shape.area() and shape.render() on each one, and every shape answers with its own
formula and its own drawing routine. Adding a new Hexagon later means writing one new
class; the loop that draws the canvas does not change.
Common mistake: confusing method overloading with polymorphism - writing three
area(Circle c), area(Rectangle r), area(Triangle t) methods on some AreaCalculator
class and calling it a day. The compiler picks the overload at compile time based on the
argument type it sees; polymorphism is the runtime picking the override based on the object
it actually holds, through a single shape.area() call site.
Try it yourself: the drawing app's loop calls shape.render() on every shape - work
through what happens the day someone adds a Group shape that contains other shapes, and
whether the same one-method-many-answers trick still holds.
Check yourself
A flight simulator and a booking site both have an Airplane class, and they share almost no fields. Which pillar explains that?