Composition Over Inheritance: Crafting More Flexible PHP Code

Sohag HasanSohag Hasan
3 min read

Composition Over Inheritance

Introduction

Imagine you're building a LEGO set. Would you prefer a massive, pre-built structure that's hard to modify, or a collection of flexible blocks that you can rearrange and combine in countless ways? In the world of object-oriented programming, the "Composition over Inheritance" principle is just like choosing those versatile LEGO blocks over a rigid, fixed model.

What is Composition Over Inheritance?

At its core, composition is about building complex objects by combining simpler, more focused objects, rather than creating deep inheritance hierarchies. It's like creating a Swiss Army knife by bringing together different specialized tools, instead of trying to create one mega-tool that does everything.

The Problem with Traditional Inheritance

Let's look at a classic inheritance example that quickly becomes problematic:

class Vehicle {
    public function startEngine() { /* ... */ }
    public function stopEngine() { /* ... */ }
    public function fly() { /* ... */ }
    public function swim() { /* ... */ }
}

class Car extends Vehicle {
    // Cars don't fly or swim, but they inherit these methods!
}

class Airplane extends Vehicle {
    // Works for flying, but what about swimming?
}

This approach leads to several issues:

  • Inherited methods that don't make sense for all subclasses

  • Rigid structure that's hard to extend

  • Violation of the Liskov Substitution Principle

Composition: A Better Approach

Let's reimagine our vehicle design using composition:

interface EngineInterface {
    public function start(): void;
    public function stop(): void;
}

interface FlyingInterface {
    public function fly(): void;
}

interface SwimmingInterface {
    public function swim(): void;
}

class StandardEngine implements EngineInterface {
    public function start(): void {
        echo "Standard engine starting...";
    }

    public function stop(): void {
        echo "Standard engine stopping...";
    }
}

class JetEngine implements EngineInterface, FlyingInterface {
    public function start(): void {
        echo "Jet engine starting...";
    }

    public function stop(): void {
        echo "Jet engine stopping...";
    }

    public function fly(): void {
        echo "Jet flying high!";
    }
}

class Vehicle {
    private $engine;
    private $flyingCapability;
    private $swimmingCapability;

    public function __construct(
        EngineInterface $engine, 
        ?FlyingInterface $flyingCapability = null, 
        ?SwimmingInterface $swimmingCapability = null
    ) {
        $this->engine = $engine;
        $this->flyingCapability = $flyingCapability;
        $this->swimmingCapability = $swimmingCapability;
    }

    public function start() {
        $this->engine->start();
    }

    public function fly() {
        if ($this->flyingCapability) {
            $this->flyingCapability->fly();
        } else {
            throw new \Exception("This vehicle cannot fly");
        }
    }
}

// Real-world usage
$car = new Vehicle(new StandardEngine());
$airplane = new Vehicle(new JetEngine(), new JetEngine());

Real-Life Analogy: The Professional Toolkit

Think of composition like a professional's toolkit:

  • A carpenter doesn't become a carpenter by inheriting all skills from a "Craftsman" class

  • Instead, they collect specific tools (skills) that can be combined as needed

  • They can add or remove tools (capabilities) without redesigning their entire approach

Benefits of Composition

  1. Flexibility: Easily add or remove behaviors

  2. Modularity: Create complex objects from simple, focused components

  3. Reduced Coupling: Objects depend on interfaces, not concrete implementations

  4. Easier Testing: Swap out components more easily

When to Use Composition

  • When behaviors can change dynamically

  • When you want to avoid deep inheritance hierarchies

  • When you need to combine capabilities in multiple ways

  • When you want to follow the Single Responsibility Principle

Common Pitfalls to Avoid

  • Don't create overly complex composition structures

  • Keep interfaces small and focused

  • Avoid too many dependencies between components

Conclusion

Composition over inheritance is like cooking: instead of creating one massive, inflexible dish, you're preparing individual ingredients that can be mixed and matched to create endless culinary possibilities.

By embracing composition, you'll write more flexible, maintainable, and scalable PHP code that can adapt to changing requirements with ease.

Quick Takeaways

  • ✅ Prefer combining behaviors over deep inheritance

  • ✅ Use interfaces to define contracts

  • ✅ Keep components small and focused

  • ✅ Think of objects as collections of capabilities

Happy coding! 🚀📦

0
Subscribe to my newsletter

Read articles from Sohag Hasan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sohag Hasan
Sohag Hasan

WhoAmI => notes.sohag.pro/author