Composition Over Inheritance: A Cross-Language Perspective for Modern Developers

In object-oriented design, few principles have aged as well as:

Favor composition over inheritance.

Popularized in Design Patterns, this principle remains foundational across modern ecosystems — from JVM languages to .NET, Python, and Swift.

While inheritance models taxonomy, composition models behavior. In practice, behavior modeling is what most real systems need.

This article explains why composition is preferred, and demonstrates it with clean, minimal examples in:

  • Kotlin
  • Java
  • Python
  • C#
  • Swift

Why Composition Wins in Real Systems

Inheritance introduces:

  • Tight coupling between parent and child
  • Fragile base class problems
  • Rigid hierarchies
  • Reduced testability

Composition provides:

  • Loose coupling
  • Better encapsulation
  • Runtime flexibility
  • Improved unit testing
  • Cleaner domain modeling

The guiding rule:

Use inheritance for “is-a” relationships.
Use composition for “has-a” relationships.


The Example: A Car and an Engine

A car has an engine.
A car is not an engine.

Let’s model that correctly in each language.


Kotlin

Kotlin encourages composition by making classes final by default.

❌ Inheritance (Incorrect Modeling)

open class Engine {
    fun start() = println("Engine starting")
}

class Car : Engine() {
    fun drive() {
        start()
        println("Driving")
    }
}

This implies Car is an Engine, which is semantically wrong.


✅ Composition (Preferred)

class Engine {
    fun start() = println("Engine starting")
}

class Car(private val engine: Engine) {
    fun drive() {
        engine.start()
        println("Driving")
    }
}

Now:

  • Car has an Engine
  • Engine can be replaced
  • Easier testing via dependency injection

Java

Modern Java architecture (especially with Spring) strongly favors composition.

❌ Inheritance

class Engine {
    void start() {
        System.out.println("Engine starting");
    }
}

class Car extends Engine {
    void drive() {
        start();
        System.out.println("Driving");
    }
}

Poor semantic relationship.


✅ Composition

class Engine {
    void start() {
        System.out.println("Engine starting");
    }
}

class Car {
    private final Engine engine;

    Car(Engine engine) {
        this.engine = engine;
    }

    void drive() {
        engine.start();
        System.out.println("Driving");
    }
}

This enables:

  • Dependency injection
  • Swappable implementations
  • Test doubles in unit tests

Python

Python’s flexibility makes composition natural and expressive.

❌ Inheritance

class Engine:
    def start(self):
        print("Engine starting")

class Car(Engine):
    def drive(self):
        self.start()
        print("Driving")

This blurs domain boundaries.


✅ Composition

class Engine:
    def start(self):
        print("Engine starting")

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

    def drive(self):
        self.engine.start()
        print("Driving")

Python benefits especially from composition because:

  • Duck typing allows flexible substitution
  • Testing with mocks is trivial
  • No rigid type hierarchy required

C# (.NET)

Modern .NET (ASP.NET Core and Clean Architecture) is composition-first.

❌ Inheritance

class Engine
{
    public void Start() => Console.WriteLine("Engine starting");
}

class Car : Engine
{
    public void Drive()
    {
        Start();
        Console.WriteLine("Driving");
    }
}

Incorrect domain modeling.


✅ Composition

class Engine
{
    public void Start() => Console.WriteLine("Engine starting");
}

class Car
{
    private readonly Engine _engine;

    public Car(Engine engine)
    {
        _engine = engine;
    }

    public void Drive()
    {
        _engine.Start();
        Console.WriteLine("Driving");
    }
}

This pattern scales naturally into:

  • Interface-based design
  • Dependency Injection containers
  • Testable services

Swift

Swift is arguably the most composition-oriented mainstream language.

❌ Inheritance

class Engine {
    func start() {
        print("Engine starting")
    }
}

class Car: Engine {
    func drive() {
        start()
        print("Driving")
    }
}

Incorrect subtype semantics.


✅ Composition

class Engine {
    func start() {
        print("Engine starting")
    }
}

class Car {
    private let engine: Engine

    init(engine: Engine) {
        self.engine = engine
    }

    func drive() {
        engine.start()
        print("Driving")
    }
}

Even more idiomatic Swift uses protocols:

protocol Engine {
    func start()
}

This avoids inheritance entirely.


Architectural Impact

Composition unlocks:

1. Testability

You can inject mock dependencies easily:

Car car = new Car(new FakeEngine());

2. Flexibility

Swap behavior without changing class hierarchy.

3. Clear Domain Modeling

A Car having an Engine is correct.
A Car being an Engine is not.

4. Better SOLID Compliance

Composition naturally supports:

  • Single Responsibility Principle
  • Open/Closed Principle
  • Dependency Inversion Principle

When Inheritance Is Appropriate

Inheritance still has valid use cases:

  • True subtype relationships
  • Template Method pattern
  • Framework extension points
  • Sealed hierarchies

But deep inheritance trees are often a code smell.


Final Takeaway

Across Kotlin, Java, Python, C#, and Swift, one principle consistently produces better systems:

Design behavior through composition.

Inheritance defines what something is.
Composition defines what something does.

Modern software systems are behavior-driven. That’s why composition dominates contemporary architecture.


codingdesignpattern

Top Rated Book On Amazon Check

Leave a Comment

Your email address will not be published. Required fields are marked *