OOP, twenty years in, isn’t a religion — it’s one tool among several. The four pillars (encapsulation, abstraction, inheritance, polymorphism) are real, but only encapsulation and polymorphism age well. Inheritance is mostly a trap.

What’s still load-bearing

Encapsulation. Hiding state behind a narrow interface lets you change the implementation without breaking callers. The actual win isn’t access modifiers — it’s that you can’t reason about a system whose state is globally readable and writable. Make state private until proven otherwise.

Polymorphism (subtype + parametric). Calling db.write(x) and dispatching to Postgres or Redis or a fake-for-tests is what makes systems testable and pluggable. This is the only OOP feature most modern code can’t replace cleanly.

What ages badly

Inheritance for code reuse. “Is-a” relationships are rare; “has-a” is the default. Deep inheritance hierarchies couple subclasses to private details of the parent (Liskov violations everywhere) and fight you on every refactor. Compose, don’t inherit — and when you do inherit, do it for interface, not implementation.

Abstract base classes for one implementation. A UserRepository interface with one PostgresUserRepository implementation is dead weight. Add the interface when the second implementation arrives.

God objects pretending to be cohesive. A User class with 40 methods has lost the plot. Domain types should be small; behavior that operates on multiple types belongs in services or free functions.

Patterns I actually reach for

  • Strategy — pluggable algorithm. Often just a function in languages with first-class functions.
  • Adapter / Facade — wrap an ugly external API behind a domain-shaped one at the boundary, not throughout.
  • Builder — for objects with many optional fields. In Python, kwargs + dataclasses cover most cases.
  • Observer / pub-sub — the moment more than two consumers care, eventize.

Patterns that are usually a smell

  • Singleton — global state with extra steps. Pass dependencies in.
  • Factory factory — if you can’t name the abstraction, the abstraction shouldn’t exist yet.
  • Visitor — the right answer when your language has no pattern matching, awkward when it does.

Method overloading vs overriding

  • Overloading = same name, different signatures, resolved at compile time. Python doesn’t really have it (use default args or singledispatch).
  • Overriding = subclass replaces parent’s method, resolved at runtime via vtable. The polymorphism mechanism that pays the rent.

Constructors, briefly

A constructor’s only job is to leave the object in a valid state. If your constructor takes 8 arguments and does I/O, the type is doing too much. Move the I/O into a factory function; split the type.