Composition vs. Inheritance
In Object-Oriented Programming, there are two primary ways to build relationships between classes and reuse code: Inheritance and Composition. While we have already covered inheritance, it's crucial to understand its alternative, composition, and to know when to choose one over the other. This choice is one of the most fundamental design decisions you will make when structuring your programs.
A general rule of thumb in the programming community is to "favor composition over inheritance." This article will explore why.
📚 Prerequisites
You should have a solid understanding of Python classes and inheritance.
🎯 Article Outline: What You'll Master
In this article, you will learn:
- ✅ The "Is-A" Relationship: A recap of how inheritance works.
- ✅ The "Has-A" Relationship: The core concept of composition, where a class is built from other classes.
- ✅ A Practical Comparison: See how to model the same problem using both approaches.
- ✅ Why Favor Composition: Understand the arguments for why composition often leads to more flexible and maintainable code.
🧠 Section 1: A Recap of Inheritance ("Is-A")
Inheritance creates an "is-a" relationship. A Dog is an Animal. This means the Dog class inherits all the attributes and methods of the Animal class, creating a tightly coupled relationship.
When it's good:
- When the child class is a true, specific type of the parent class.
- When you want to reuse a large amount of code and behavior from the parent.
- When you want to use polymorphism across a family of related classes.
When it can be problematic:
- Inheritance is rigid. The relationship is defined when you write the code and cannot be changed at runtime.
- Changes to the parent class can easily break the child class.
- Deep, multi-level inheritance hierarchies can become extremely complex and hard to reason about (the "fragile base class" problem).
💻 Section 2: Understanding Composition ("Has-A")
Composition creates a "has-a" relationship. Instead of a class being another class, it has another class as a component. A Car has an Engine.
In this model, a container class holds instances of other classes and delegates tasks to them.
When it's good:
- When you want to build complex objects out of smaller, independent, and reusable components.
- When you need flexibility. You can change the components at runtime.
- When you want to avoid the tight coupling of inheritance.
Let's model a Robot. A robot isn't a type of arm or a type of leg, but it has arms and legs. This is a perfect case for composition.
# composition_example.py
class GripperArm:
"""A component class representing a robot's arm."""
def pick_up(self, item):
print(f"Picking up {item} with the gripper.")
class Leg:
"""A component class representing a robot's leg."""
def move_forward(self, distance):
print(f"Moving forward {distance} meters.")
class Robot:
"""The container class that is 'composed' of other objects."""
def __init__(self, name):
self.name = name
# The Robot "has-a" GripperArm and two Legs
self.arm = GripperArm()
self.left_leg = Leg()
self.right_leg = Leg()
def grab(self, item):
"""The Robot delegates the 'grab' task to its arm component."""
print(f"{self.name} is about to grab something.")
self.arm.pick_up(item)
def walk(self, distance):
"""The Robot delegates the 'walk' task to its leg components."""
print(f"{self.name} is about to walk.")
self.left_leg.move_forward(distance)
self.right_leg.move_forward(distance)
# --- Let's use our composed Robot ---
my_robot = Robot("Bender")
my_robot.walk(10)
my_robot.grab("a shiny object")
The Robot class doesn't know the details of how an arm or leg works. It just knows that it has these components and that it can delegate tasks to them.
🛠️ Section 3: A Head-to-Head Comparison
Let's model an employee who can do work.
Inheritance Approach:
Here, we might say a SalariedEmployee is a Worker.
class Worker:
def work(self):
print("I am working.")
class SalariedEmployee(Worker): # "Is-A" relationship
def calculate_pay(self):
print("Calculating pay for a salaried employee.")
# Problem: What if we have a contract employee who also works?
# Do we make ContractEmployee inherit from Worker too?
# What if their work method is slightly different?
This seems okay, but the relationship is rigid.
Composition Approach:
Here, we say an Employee has a WorkBehavior.
class SalariedWorkBehavior:
def work(self):
print("Working 8 hours a day.")
class HourlyWorkBehavior:
def work(self):
print("Clocking in and working by the hour.")
class Employee:
def __init__(self, work_behavior):
# "Has-A" relationship
self.work_behavior = work_behavior
def do_work(self):
# Delegate the work task to the component
self.work_behavior.work()
# --- Create different types of employees ---
salaried_worker = Employee(SalariedWorkBehavior())
hourly_worker = Employee(HourlyWorkBehavior())
print("Salaried worker:")
salaried_worker.do_work()
print("\nHourly worker:")
hourly_worker.do_work()
This is much more flexible! We can create any number of work behaviors and "plug them into" an Employee object without changing the Employee class itself. We could even change an employee's work behavior at runtime.
✨ Conclusion & Key Takeaways
While inheritance is a powerful and necessary tool, composition often provides a more flexible and robust way to structure your code. It encourages you to build small, self-contained components that can be combined in various ways.
Let's summarize the key takeaways:
- Inheritance ("Is-A"): Use for clear, hierarchical relationships where a subclass is a true subtype of the parent.
- Composition ("Has-A"): Use for building complex objects from smaller, independent parts.
- Flexibility: Composition is generally more flexible than inheritance because you can change components at runtime.
- Simplicity: Composition helps keep class hierarchies flat and simple, avoiding the complexity of deep inheritance chains.
- Favor Composition Over Inheritance: When designing your classes, start by thinking about what an object has. Only use inheritance when the "is-a" relationship is clear and unambiguous.
➡️ Next Steps
This is the second-to-last article in our series on Advanced OOP. In our final article, we'll look at "Metaclasses," a deep and powerful feature of Python that allows you to control the creation of classes themselves.
Happy designing!