The Problem With How OOP Is Often Taught

Many developers learn object-oriented programming through inheritance.

The lesson usually looks like this:

class Animal:
    pass

class Bird(Animal):
    pass

class Eagle(Bird):
    pass

Or:

class Vehicle:
    pass

class Car(Vehicle):
    pass

class ElectricCar(Car):
    pass

At first, this feels natural.

An eagle is a bird.
A bird is an animal.

An electric car is a car.
A car is a vehicle.

The hierarchy matches how humans categorize things.

For small examples, this works fine.

The problems start when systems become larger.


The Real Question Is Usually About Behavior

Most software does not care what something is.

It cares what something can do.

Imagine a payment system.

Does the checkout page care whether payment is handled by:

  • Stripe
  • PayPal
  • Visa
  • A future payment provider

Not really.

The checkout page only cares about one thing:

Can this thing process a payment?

That is behavior.

Behavior is often more important than inheritance.


A Better Example Than Birds

Many programming books use birds.

For example:

Bird
 |- Eagle
 |- Sparrow
 `- Penguin

Then they discover:

Penguins cannot fly.

And the hierarchy becomes awkward.

A more useful example is transportation.

Imagine:

Car
Truck
Motorcycle
Boat
Drone

Now ask:

Which ones can move?

Answer:

All of them.

Now ask:

Which ones can carry cargo?

Answer:

Truck
Boat
Some drones

Now ask:

Which ones can operate autonomously?

Answer:

Some cars
Some trucks
Some drones

Notice something important.

These abilities do not follow the inheritance tree.

They cross the hierarchy.


The Inheritance Explosion Problem

Suppose we start here.

Vehicle
|- Car
`- Truck

Easy.

Then somebody asks for electric vehicles.

Vehicle
|- Car
|  |- GasCar
|  `- ElectricCar
|
`- Truck
   |- GasTruck
   `- ElectricTruck

Still manageable.

Then somebody wants autonomous driving.

AutonomousGasCar
AutonomousElectricCar
AutonomousGasTruck
AutonomousElectricTruck

Then hybrid systems.

AutonomousHybridTruck
AutonomousHybridCar
...

The hierarchy keeps growing.

Every new feature multiplies the number of classes.

This is called a combinatorial explosion.

The problem is not inheritance itself.

The problem is using inheritance to model multiple independent behaviors.


Real Vehicles Are Built From Parts

Real vehicles are not inheritance trees.

They are assemblies.

A vehicle may have:

  • An engine
  • A battery
  • A navigation system
  • An autonomous driving system
  • A braking system

These parts evolve independently.

You can replace the engine without replacing the entire vehicle.

You can upgrade the navigation system without redesigning the car.

Real systems are composed from parts.

Good software often works the same way.


Composition Instead Of Inheritance

Instead of saying:

Car IS-A ElectricCar

we say:

Car HAS-A engine

That small change is powerful.

Example:

class Car:

    def __init__(self, engine):
        self.engine = engine

    def start(self):
        self.engine.start()

The car no longer cares about engine type.

It only cares that the engine can start.


Focus On Behavior

Instead of asking:

What type is this?

Ask:

What behavior do I need?

Example:

from typing import Protocol

class Engine(Protocol):

    def start(self) -> None:
        ...

This says:

Anything that can start
may be used as an engine.

Now we can create different implementations.

class GasEngine:

    def start(self):
        print("Starting gas engine")
class ElectricMotor:

    def start(self):
        print("Starting electric motor")
class HydrogenEngine:

    def start(self):
        print("Starting hydrogen engine")

The car does not change.

car = Car(ElectricMotor())
car.start()

Later:

car = Car(HydrogenEngine())
car.start()

Still works.

The behavior stayed the same.

The implementation changed.


This Is Why Protocols Exist

Protocols focus on behavior.

They ask:

Can you do the job?

Not:

What family do you belong to?

Imagine hiring a software engineer.

You care about:

  • Can they write code?
  • Can they debug systems?
  • Can they communicate?

You do not care whether they graduated from the same school as everyone else.

Protocols work similarly.

If an object behaves correctly, it can participate.

Inheritance becomes optional.


Dependency Inversion In Plain English

This phrase sounds complicated.

The idea is simple.

Bad:

class Car:

    def start(self):
        engine = GasEngine()
        engine.start()

The car is permanently tied to one engine.

You cannot easily replace it.

Better:

class Car:

    def __init__(self, engine):
        self.engine = engine

Now the dependency is provided from outside.

car = Car(ElectricMotor())

Or:

car = Car(HydrogenEngine())

The car depends on behavior.

Not implementation.

That is dependency inversion.


The Same Idea Appears Everywhere

This pattern is not limited to cars.

Databases

Bad:

class UserService:

    def get_user(self):
        postgres.query(...)

Now the service depends on PostgreSQL.

Changing databases becomes difficult.

Better:

class UserService:

    def __init__(self, repository):
        self.repository = repository

Now the service depends on behavior.

The repository can be:

PostgresRepository
MySQLRepository
MongoRepository
FakeRepository

The service stays the same.


Notifications

Bad:

class AlertService:

    def send(self):
        email.send(...)

Better:

class AlertService:

    def __init__(self, notifier):
        self.notifier = notifier

Now you can swap:

EmailNotifier
SlackNotifier
SMSNotifier

without touching the alert service.


Architecture Is About Change

Most architecture discussions are really discussions about change.

Ask:

What changes often?
What changes rarely?

Usually:

Changes often:

  • Databases
  • APIs
  • Cloud providers
  • Message queues
  • Payment systems

Changes slowly:

  • Business rules
  • Core workflows
  • Domain logic

Good architecture protects the stable parts.


Stable Core, Replaceable Edges

Think of a system like this:

Application Logic
        |
        v
     Contract
        |
        v
Implementation

Example:

Checkout Service
        |
        v
Payment Processor
        |
        v
Stripe

Later:

Checkout Service
        |
        v
Payment Processor
        |
        v
PayPal

Only the edge changed.

The core remained stable.

That is the goal.


Composition Over Inheritance

This advice appears everywhere because it solves real problems.

Inheritance models:

IS-A

Examples:

Dog IS-A Animal

Car IS-A Vehicle

Composition models:

HAS-A

Examples:

Car HAS-A Engine

Application HAS-A Database

Checkout Service HAS-A Payment Processor

As systems grow, “has-a” relationships tend to be more flexible than “is-a” relationships.


When Inheritance Is Still Useful

Inheritance is not evil.

It is just overused.

Inheritance works well when:

  • The hierarchy is stable
  • The relationship is truly “is-a”
  • Shared behavior belongs in one place

Example:

from abc import ABC, abstractmethod

class BaseStorage(ABC):

    def connect(self):
        print("Connecting")

    @abstractmethod
    def save(self, data):
        pass

All storage implementations share connection logic.

Inheritance makes sense here.

The key is moderation.


A Practical Rule

When designing a system, ask:

Am I modeling a type?

Or am I modeling a capability?

If you are modeling a capability:

Can send notifications
Can process payments
Can save data
Can start an engine

Behavior is usually more important than inheritance.


The Mental Model

Think of software as a collection of replaceable parts.

Just like a real car.

A car can swap:

  • Engine
  • Tires
  • Navigation system
  • Battery

without becoming a different car.

Good software should work the same way.

Instead of building giant inheritance trees:

Vehicle
 -> Car
    -> ElectricCar
       -> AutonomousElectricCar

build systems from behaviors and components.

Car
 |- Engine
 |- Navigation
 |- Autopilot
 `- Brakes

The result is easier to test.

Easier to change.

Easier to understand.

And much more likely to survive future requirements.