Composition Over Inheritance
"Composition over Inheritance" (also known as "favor composition over inheritance") is a design principle in object-oriented programming that suggests that classes should achieve new functionality by composing objects of existing classes (i.e., by containing instances of other classes) rather than by inheriting from them. This principle promotes greater flexibility, reduced coupling, and better code reusability.
The Problem with Inheritance
While inheritance (extends
keyword in Java) is a fundamental OOP concept for achieving code reuse and polymorphism, it can lead to several issues, especially in complex systems:
- Tight Coupling: Inheritance creates a strong, rigid relationship between the parent (superclass) and child (subclass). The subclass becomes dependent on the superclass's implementation details. Changes in the superclass can unexpectedly affect subclasses (the "fragile base class problem").
- Liskov Substitution Principle (LSP) Violations: Sometimes, developers use inheritance purely for code reuse, even when the "is-a" relationship (which inheritance implies) doesn't strictly hold true. This can lead to subclasses that cannot be substituted for their superclass without breaking the program's correctness.
- Limited Flexibility: A class can only inherit from one superclass in Java (single inheritance). If a class needs functionality from multiple unrelated sources, inheritance becomes problematic.
- Exposing Superclass Internals: Inheritance often exposes the superclass's protected or even private members to subclasses, making encapsulation weaker.
- Difficulty in Changing Behavior at Runtime: The behavior inherited from a superclass is fixed at compile time. It's difficult to change an object's inherited behavior dynamically.
What is Composition?
Composition involves building complex objects by combining simpler, existing objects. Instead of inheriting behavior from a base class, a class acquires new behavior by holding an instance of another class and delegating tasks to that instance. This creates a "has-a" relationship.
Example: A Car
"has an" Engine
, rather than a Car
"is an" Engine
.
Advantages of Composition
- Flexibility and Adaptability:
- Runtime Behavior Changes: Behaviors can be changed at runtime by swapping out the composed object with another object that implements the same interface. This is a core idea behind strategies and dependency injection.
- Multiple Behaviors: A class can compose multiple different objects, effectively achieving multiple behaviors without the limitations of single inheritance.
- Loose Coupling:
- Reduced Dependencies: The composing class interacts with the composed object through its interface, not its implementation details. This reduces the dependency between classes.
- Encapsulation: The internal implementation of the composed object is hidden from the composing class, promoting better encapsulation.
- Easier Testing: Because components are less coupled, they are generally easier to test in isolation.
- Better Code Reusability: Individual components (the composed objects) can be reused across different contexts by being composed into various classes.
- Better Follows Real-World Relationships: Often, real-world relationships are "has-a" (composition) rather than "is-a" (inheritance).
Disadvantages of Composition
- Increased Number of Objects: You might end up with more objects and classes compared to a pure inheritance hierarchy, which could seem more complex initially.
- Delegation Overhead: You often need to write "delegation" code – methods in the composing class that simply call methods on the composed object. This can sometimes feel like boilerplate, though IDEs and sometimes language features can help.
- Interface Dependency: The effectiveness of composition relies heavily on well-defined interfaces for the composed objects.
Illustrative Example: A Car
and its Engine
Using Inheritance (Less Ideal for this scenario):
// Not a good design for typical car/engine relationship
class Engine {
public void start() {
System.out.println("Engine starts.");
}
public void stop() {
System.out.println("Engine stops.");
}
}
class Car extends Engine { // Problem: A Car "is an" Engine, which isn't true
public void drive() {
start(); // Inherited method
System.out.println("Car is driving.");
}
}
// Problem: What if we want to change the Car's engine type at runtime?
// Or have different engine types like ElectricEngine, PetrolEngine?
Using Composition (Preferred):
// Define an interface for the behavior we need
interface Startable {
void start();
void stop();
}
// Concrete implementations of the behavior
class PetrolEngine implements Startable {
@Override
public void start() {
System.out.println("Petrol engine starts with a roar.");
}
@Override
public void stop() {
System.out.println("Petrol engine stops.");
}
}
class ElectricEngine implements Startable {
@Override
public void start() {
System.out.println("Electric engine hums to life.");
}
@Override
public void stop() {
System.out.println("Electric engine powers down silently.");
}
}
// The Car class composes an Engine (or any Startable)
class Car {
private Startable engine; // Car "has a" Startable (interface type)
// Constructor injection: engine is provided from outside
public Car(Startable engine) {
this.engine = engine;
}
// Setter injection: allow changing engine at runtime
public void setEngine(Startable engine) {
this.engine = engine;
}
public void drive() {
if (engine != null) {
engine.start(); // Delegate to the composed object
System.out.println("Car is driving.");
} else {
System.out.println("No engine to drive the car.");
}
}
public void stopDriving() {
if (engine != null) {
engine.stop(); // Delegate to the composed object
System.out.println("Car has stopped.");
}
}
public static void main(String[] args) {
Car petrolCar = new Car(new PetrolEngine());
petrolCar.drive();
petrolCar.stopDriving();
System.out.println("--- Swapping Engine ---");
Car electricCar = new Car(new ElectricEngine());
electricCar.drive();
electricCar.stopDriving();
// Example of changing engine at runtime (if allowed by design)
petrolCar.setEngine(new ElectricEngine());
petrolCar.drive();
}
}
In the composition example, the Car
class doesn't inherit from Engine
. Instead, it contains an instance of an object that implements the Startable
interface. This allows us to easily swap out different types of engines (e.g., PetrolEngine
, ElectricEngine
) without changing the Car
class itself, achieving greater flexibility.
When to Choose Which
-
Choose Inheritance when:
- There is a clear and undeniable "is-a" relationship (e.g., a
Dog
is-a
Animal
). - You want to reuse a common implementation and extend it.
- The base class and subclasses are tightly coupled and form a single, cohesive unit.
- You want to leverage polymorphism where subclasses are directly substitutable for the superclass (LSP holds true).
- There is a clear and undeniable "is-a" relationship (e.g., a
-
Choose Composition when:
- There is a "has-a" relationship (e.g., a
Car
has-an
Engine
). - You want to achieve flexibility and the ability to change behavior at runtime.
- You want to reduce coupling between classes.
- You need to reuse behavior from multiple, unrelated sources.
- The behavior you need is independent of the class hierarchy.
- There is a "has-a" relationship (e.g., a
In modern OOP design, "Composition over Inheritance" is generally favored as it leads to more flexible, maintainable, and robust systems. Inheritance should be used judiciously, typically for creating well-defined hierarchies where the "is-a" relationship holds strong and the benefits of shared implementation outweigh the potential for tight coupling.