Menu
Home Articles Bookmarks Experience Profiles About Work With Me
Tree with wide branching structure against a dramatic sky
Software Engineering Apr 23, 2026 • 15 min read

Inheritance Is the First Superpower Every Developer Abuses

A Dog IS an Animal. A Square IS a Shape. Inheritance lets you build family trees of code. But deep hierarchies are fragile, the diamond problem is real, and composition is almost always the better choice. This lesson teaches you when to extend and when to compose.

Share:
Lee Foropoulos

Lee Foropoulos

15 min read

Continue where you left off?
Text size:

Contents

Complete Guide | Lesson 4: ClassesLesson 5: Inheritance & Composition → Lesson 6: Interfaces (Apr 30)

Inheritance is the first superpower every developer learns. It's also the first one they abuse.

The pitch is seductive: define behavior once in a parent class, and every child class gets it for free. A Dog extends Animal. A Sedan extends Vehicle. A CheckingAccount extends BankAccount. Write the shared behavior once, share it everywhere, never repeat yourself. What could go wrong?

Quite a lot, actually. And this lesson exists because understanding when NOT to use inheritance is more valuable than understanding how to use it.

This is Lesson 5, and it maps to Binah on the Tree of Life. Binah means "Understanding." Where Chokmah (Lesson 4) was the flash of creative insight that produced blueprints, Binah is the receptive form that gives structure and limitation. Binah says: here are the rules. Here are the constraints. Here is where one thing ends and another begins. In programming, Binah teaches you relationships between classes: when one thing truly IS another thing, and when it merely HAS another thing. Getting this distinction right is the difference between code that scales and code that crumbles.

Inheritance is the first superpower every developer learns. Composition is the one they eventually wish they'd learned first.

The Family Tree

Inheritance creates a family tree of classes. A parent class (also called a base class or superclass) defines shared properties and methods. Child classes (subclasses) inherit everything from the parent and can add their own.

1CLASS Animal
2    name: String
3    age: Integer
4
5    METHOD eat()
6        PRINT this.name + " is eating"
7    END METHOD
8
9    METHOD sleep()
10        PRINT this.name + " is sleeping"
11    END METHOD
12END CLASS
13
14CLASS Dog EXTENDS Animal
15    breed: String
16
17    METHOD bark()
18        PRINT this.name + " says: Woof!"
19    END METHOD
20
21    METHOD fetch(item)
22        PRINT this.name + " fetches the " + item
23    END METHOD
24END CLASS
25
26CLASS Cat EXTENDS Animal
27    isIndoor: Boolean
28
29    METHOD purr()
30        PRINT this.name + " is purring"
31    END METHOD
32
33    METHOD ignoreOwner()
34        PRINT this.name + " stares at you with contempt"
35    END METHOD
36END CLASS

Dog inherits eat() and sleep() from Animal without rewriting them. It adds bark() and fetch() on its own. Cat inherits the same base methods but adds purr() and ignoreOwner(). (The cat one is the most realistic method in this entire series.)

Branching tree structure with colorful interconnected paths
Inheritance creates branching hierarchies. Animal branches into Dog and Cat. Vehicle branches into Car and Truck. The higher up the tree you make a change, the more descendants are affected. This is both the power and the danger.

The IS-A Test

Before you write class X extends Y, ask yourself: "Is X genuinely a type of Y?"

  • A Dog IS an Animal. Valid.
  • A CheckingAccount IS a BankAccount. Valid.
  • A Car IS an Engine. Invalid. A car HAS an engine. Different relationship entirely.
  • A Square IS a Rectangle. Dangerous. Mathematically true. Programmatically, it violates Liskov Substitution (more on that in Lesson 7).

If the IS-A test fails, inheritance is the wrong tool. Use composition instead.

IS-A
is the only valid test for inheritance. If you can't honestly say 'X is a type of Y,' don't use inheritance. Use composition (HAS-A) instead.

The Fragile Base Class Problem

Here's where the fairy tale ends. Inheritance creates coupling between parent and child classes. Every child depends on every detail of its parent. Change the parent, and every child might break. This is called the fragile base class problem, and it's the reason experienced developers are cautious about deep inheritance hierarchies.

Imagine you have this hierarchy:

1Animal
2  └── Pet
3        └── Dog
4              └── ServiceDog
5                    └── GuideDog

Five levels deep. Now suppose you need to change how Pet.eat() works. That change cascades through Dog, ServiceDog, and GuideDog. Each might have overridden eat() with behavior that depends on the old implementation. Each might break in subtle ways that don't show up until runtime.

Steve McConnell in Code Complete recommended a maximum inheritance depth of 3 levels. The Gang of Four (Gamma, Helm, Johnson, Vlissides) in their Design Patterns book put it more bluntly: "Favor composition over inheritance." That line, published in 1994, changed the trajectory of software design.

Warning Signs Your Hierarchy Is Too Deep

  • More than 3 levels of inheritance
  • You're creating subclasses just to override one method
  • Your class names look like FlyingFireBreathingWizardWarrior
  • Changing a base class requires testing 15 subclasses
  • You find yourself fighting the parent class instead of extending it If you see any of these, stop inheriting and start composing.
3 levels
maximum recommended inheritance depth according to Code Complete. Beyond that, the coupling becomes so tight that changes propagate unpredictably through the hierarchy.

The Rectangle-Square Disaster

Here's the classic example that breaks every student's brain:

A square is a rectangle where width equals height. So naturally, Square extends Rectangle. Right?

1CLASS Rectangle
2    width: Integer
3    height: Integer
4
5    METHOD setWidth(w)
6        this.width = w
7    END METHOD
8
9    METHOD setHeight(h)
10        this.height = h
11    END METHOD
12
13    METHOD area()
14        RETURN this.width * this.height
15    END METHOD
16END CLASS
17
18CLASS Square EXTENDS Rectangle
19    METHOD setWidth(w)
20        this.width = w
21        this.height = w    // Must keep them equal!
22    END METHOD
23
24    METHOD setHeight(h)
25        this.width = h     // Must keep them equal!
26        this.height = h
27    END METHOD
28END CLASS

Now imagine code that expects a Rectangle:

1FUNCTION doubleWidth(rect)
2    rect.setWidth(rect.width * 2)
3    ASSERT rect.area() == rect.width * rect.height
4END FUNCTION

Pass a Rectangle: width doubles, height stays. Area equals width times height. Works.

Pass a Square: width doubles, but setWidth also sets height. Now height changed when only width should have. The assertion fails. The code is wrong, even though a square IS mathematically a rectangle.

This is the Liskov Substitution Principle in action (which we'll cover formally in Lesson 7): a subclass must be substitutable for its parent without breaking behavior. Square violates this, and it's the reason experienced developers never use this inheritance without pausing.

Composition Over Inheritance

If inheritance is a family tree, composition is Lego blocks. Instead of building a rigid hierarchy, you assemble objects from interchangeable parts.

Inheritance carves from a single block of marble. Composition snaps together Lego bricks. One gives you Michelangelo's David. The other gives you anything you can imagine, and you can rebuild it tomorrow.

HAS-A Relationships

Instead of Dog extends SwimmingAnimal, try Dog has a SwimmingBehavior:

1CLASS SwimmingBehavior
2    METHOD swim()
3        PRINT "Swimming through water"
4    END METHOD
5END CLASS
6
7CLASS FlyingBehavior
8    METHOD fly()
9        PRINT "Soaring through the air"
10    END METHOD
11END CLASS
12
13CLASS Dog
14    name: String
15    swimming: SwimmingBehavior = new SwimmingBehavior()
16
17    METHOD swim()
18        this.swimming.swim()
19    END METHOD
20END CLASS
21
22CLASS Duck
23    name: String
24    swimming: SwimmingBehavior = new SwimmingBehavior()
25    flying: FlyingBehavior = new FlyingBehavior()
26
27    METHOD swim()
28        this.swimming.swim()
29    END METHOD
30
31    METHOD fly()
32        this.flying.fly()
33    END METHOD
34END CLASS

A Dog can swim. A Duck can swim AND fly. We didn't need a SwimmingAnimal and a FlyingSwimmingAnimal hierarchy. We just composed behaviors. Want a FlyingFish? Give it SwimmingBehavior and FlyingBehavior. No new classes in the hierarchy needed.

Colorful building blocks assembled into a creative structure
Composition lets you build anything from interchangeable parts. Need a duck? Swimming plus flying. Need a penguin? Swimming minus flying. No rigid hierarchy required.

The Game Character Example

Imagine building characters for a role-playing game with inheritance:

1Character
2  └── Warrior
3  └── Mage
4  └── Archer
5  └── HealingWarrior (extends Warrior? extends Healer?)
6  └── FireMageArcher (extends... um...)

This falls apart immediately. What about a character that's both a warrior and a healer? Multiple inheritance? Most languages don't allow it.

With composition:

1CLASS Character
2    name: String
3    abilities: List<Ability> = []
4
5    METHOD addAbility(ability)
6        this.abilities.add(ability)
7    END METHOD
8
9    METHOD useAbility(abilityName)
10        FOR EACH ability IN this.abilities
11            IF ability.name == abilityName THEN
12                ability.execute(this)
13            END IF
14        END FOR
15    END METHOD
16END CLASS
17
18// Create any combination
19hero = new Character("Gandalf")
20hero.addAbility(new MeleeAttack())
21hero.addAbility(new FireMagic())
22hero.addAbility(new Healing())
23// Gandalf is now a melee-fighting fire mage who can heal
16 of 23
Gang of Four design patterns favor composition over inheritance. The founders of design patterns were telling us since 1994: compose, don't inherit.

The Diamond Problem

Here's the scenario that made language designers lose sleep: if B extends A and C extends A, what happens when D extends both B and C?

1        A
2       / \
3      B   C
4       \ /
5        D

If A has a method greet(), and both B and C override it differently, which version does D get?

Different languages handle this differently:

C++: Allows it. Uses "virtual inheritance" to resolve ambiguity. Complex and error-prone.

Java and C#: Forbid it entirely. You can only extend one class. Use interfaces (Lesson 6) for multiple contracts.

Python: Allows it. Uses Method Resolution Order (MRO), a linearization algorithm that defines a predictable lookup sequence. Still confusing.

Go and Rust: Skip class inheritance entirely. Use composition and interfaces (traits) instead.

The trend is clear: modern languages are moving away from inheritance and toward composition and interfaces. The diamond problem is one of the main reasons why.

When to Inherit vs When to Compose

Use inheritance when:

  • There is a genuine, permanent IS-A relationship
  • The hierarchy is shallow (3 levels max)
  • Subclasses genuinely specialize the parent without breaking its contract

Use composition when:

  • You need multiple behaviors (can't multiply inherit)
  • The relationship is HAS-A, not IS-A
  • You need flexibility to swap behaviors at runtime
  • You're unsure (when in doubt, compose)

Default to composition. Upgrade to inheritance only when it clearly, unambiguously fits.

"Inheritance is not a tool for code reuse. It's a tool for expressing genuine type relationships. If you're inheriting just to avoid retyping methods, you're building a house of cards." This principle took the industry two decades to fully internalize.

When in doubt, compose. You can always refactor composition into inheritance later if a genuine IS-A relationship emerges. Refactoring inheritance into composition is much harder and usually breaks things.
Diverging paths in a forest representing different design choices
Every time you create a new class, you face this fork: does it extend an existing class, or does it contain one? The path you choose determines whether your code is flexible or fragile.
Lesson 5 Practice 0/6
How was this article?

Share

Link copied to clipboard!

You Might Also Like

Lee Foropoulos

Lee Foropoulos

Business Development Lead at Lookatmedia, fractional executive, and founder of gotHABITS.

🔔

Never Miss a Post

Get notified when new articles are published. No email required.

You will see a banner on the site when a new post is published, plus a browser notification if you allow it.

Browser notifications only. No spam, no email.

0 / 0