Skip to main content

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.

model
Abstraction
AirplanespeedaltituderollAngleseatscrewfuelLoadsimulatorspeed, altitudebookingseats

One real thing, several honest partial models. Keep the details your context needs and drop the rest.

hide
Encapsulation
PUBLIC+ start() + steer()PRIVATE- crankshaft- fuelMixture- ignitionTiming

A small public surface you promise to keep stable, and private internals you reserve the right to rewrite on a Tuesday.

reuse
Inheritance
Animaleat()sleep()Catmeow()Dogbark()

A subclass gets the parent members for free and adds only what differs. The bill: it inherits the whole interface, useful parts and all.

dispatch
Polymorphism
for a in bag: a.makeSound()Cat"Meow!"Dog"Woof!"

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.

Airplane- speed- altitude- rollAngle- pitchAngle- yawAngle+ fly()Airplane- seats+ reserveSeat(n)flight simulatorbooking system
Different models of the same real-world object.

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.

Airport...+ accept(v: FlyingTransport)«interface»FlyingTransport+ fly(origin, dest, people)Helicopter...+ fly(origin, dest, people)Airplane...+ fly(origin, dest, people)Gryphon...+ fly(origin, dest, people)Simple arrows indicatethat one class dependson the otherInterfaces in UML lookalmost like classes, butonly have methodsEmpty triangle headsand dashed lines meana class implementsan interface
implementsuses
Several classes implementing one interface. The airport never learns which is which.

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.

AnimalCat«interface»FourLegged+ run(destination)«interface»OxygenBreather+ breathe()
One superclass, any number of interfaces.

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.

«abstract»Animal...+ makeSound()Cat...+ makeSound()Dog...+ makeSound()print("Meow!")print("Woof!")Abstract class andmethod names arein italics
Call 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

Question 1 of 4

A flight simulator and a booking site both have an Airplane class, and they share almost no fields. Which pillar explains that?

0%0 of 122 pages studied