Hello! In our last lesson, we explored the Dependency Inversion Principle, which taught us to make our high-level business logic depend on abstractions, not concrete details. This was a crucial step towards building decoupled, flexible systems.
Today, we'll build directly on that idea by tackling a core principle of object-oriented design: Composition over Inheritance. You'll learn why blindly reaching for inheritance to reuse code can lead to rigid and fragile designs, and how composing objectsโpiecing them together from independent componentsโcreates far more flexible and maintainable software. This principle is a cornerstone of the "architectural mindset" you're aiming to develop.
By the end of this lesson, you will be able to explain the principle of Composition over Inheritance and refactor a design to use it effectively.
1. The Core Idea: "Is A" vs. "Has A"
At its heart, the choice between inheritance and composition comes down to the relationship you want to model.
- Inheritance models an "is a" relationship. A
Caris aVehicle. ADogis anAnimal. The subclass is a specific version of the superclass. - Composition models a "has a" relationship. A
Carhas anEngine. AHousehas aBathroom. The object contains other objects to fulfill its responsibilities.

While inheritance is a fundamental OOP concept, its overuse for simple code reuse is a common design trap. Let's explore why.
2. The Problems with Overusing Inheritance
Inheritance can seem like the quickest way to share functionality, but it creates a very strong, static bond between classes that can become a significant liability as your system evolves.
Problem 1: The Combinatorial Explosion
What happens when you need to mix and match behaviors? Your class hierarchy can explode.
Why Favor Object Composition Over Class Inheritance? A Deep Dive
The following video uses a Vehicle example to perfectly demonstrate how an inheritance-based design quickly becomes unmanageable when new, combined features are required.
Watch from 00:23 to 08:41. Pay close attention to how the introduction of an 'amphibious vehicle' and then a 'floating aircraft' breaks the simple inheritance model and forces code duplication and design compromises.
As you saw, modeling vehicles that can drive, float, and fly leads to a "combinatorial explosion" of classes (AmphibiousVehicle, FlyingCar, FloatingAircraft, etc.), with significant code duplication and complexity.
Problem 2: Rigidity and Unwanted Baggage
Inheritance forces a subclass to accept everything from its parent, even functionality it doesn't need. This creates tight coupling and makes the system rigid.
This video from CodeAesthetic illustrates a different but equally important flaw using an Image class hierarchy.
Watch from 00:28 to 03:28. Notice how creating a DrawableImage class that inherits from a base Image forces it to have load() and save() methods, even though they don't make sense for this new subclass.
This rigidity means that changing the base class is risky (the "Fragile Base Class Problem"), and refactoring the hierarchy to accommodate new requirements often involves expensive, system-wide changes.
3. The Alternative: Building with Composition
Instead of inheriting behavior, we can compose an object with the behaviors it needs. We build flexible objects by plugging in components.
Let's see how the two problematic examples are solved with composition.
Why Favor Object Composition Over Class Inheritance? A Deep Dive
First, let's revisit the Vehicle example and see how it's refactored to use composition.
Watch from 10:04 to 18:04. The key shift is that the Vehicle class no longer is a type of vehicle; instead, it has a set of 'moving abilities' (components). See how this allows for any combination of behaviors without creating new classes.
Now, let's see the Image example refactored. This clip also highlights how interfacesโthe abstractions we studied in the last lessonโare the glue that makes composition so powerful.
Watch from 03:28 to 07:57. Observe how the Image class is separated from the file format logic. The key takeaway is how interfaces are used to define contracts (e.g., ImageFile with load() and save()) that different components can implement, fully decoupling them.
These refactored designs are far more flexible. They follow the Open/Closed Principle: we can add new behaviors (a new moving ability, a new image format) by creating new component classes without modifying the Vehicle or Image classes that use them. This is possible because the main classes depend on abstractions (interfaces or base classes for the components), just as we learned in the Dependency Inversion Principle.
4. A Practical Guide to Refactoring
Knowing the "why" and "what" is great, but your goal is to develop an architectural mindset, which requires knowing "how". Let's look at a structured process for refactoring from inheritance to composition.
The article below provides an excellent, step-by-step recipe. It's written in Kotlin, but the principles are universal and the code is very readable.
Composition over Inheritance: My refactoring recipe
This article, 'Composition over Inheritance: My refactoring recipe', provides an actionable, six-step process for converting a rigid abstract class hierarchy into a flexible, component-based design.
Read the article, focusing on the six-step recipe. Don't worry too much about the specific Kotlin syntax; focus on the process: Read 'Expectation vs Reality': This section summarizes the key downsides of inheritance. Study the 'Refactoring abstract classes: My recipe' section: This contains the core of the lesson. Follow the GameStateSaver example through these steps: Step 1: Analyze the existing hierarchy. Step 2: Convert the abstract base class to a concrete class, replacing abstract methods with constructor parameters for interfaces. Step 3 & 4: Implement those interfaces by extracting the logic from the old subclasses. Step 5: Refine the new components and their APIs. Step 6: Assemble the final objects by 'composing' the different components together.
This recipe provides a clear mental model for breaking down monolithic, inherited logic into small, reusable, and testable components.
Test your understanding!
Imagine you're designing a notification system. Your initial design uses inheritance:
abstract class Notifier {
abstract function send(message);
}
class EmailNotifier extends Notifier {
function send(message) {
// logic to send email
}
}
class SMSNotifier extends Notifier {
function send(message) {
// logic to send SMS
}
}
Now, a requirement comes in: "Send a high-priority alert via both Email and SMS."
- Why is creating an
EmailAndSMSNotifierclass using inheritance a bad idea? - Using the principles of composition, how would you redesign the system to handle this requirement and be ready for future ones (e.g., "send via Email and Push Notification")? Describe the classes and interfaces you would create.
Show answer
-
Creating
EmailAndSMSNotifiervia inheritance is problematic because it leads to the "combinatorial explosion" we discussed. If you later add aPushNotifier, you would then need anEmailAndPushNotifier, anSMSAndPushNotifier, and anEmailAndSMSAndPushNotifier. The number of classes grows exponentially, and they would contain duplicated logic. -
A composition-based design would look like this:
- Define an Abstraction: Keep a simple, clear interface.
interface INotifier { function send(message); } - Create Concrete Components: Your original classes become small, focused components that implement this interface.
class EmailNotifier implements INotifier { ... } class SMSNotifier implements INotifier { ... } class PushNotifier implements INotifier { ... } - Create a Composer/Manager: Create a class that has a list of notifiers and delegates the
sendcall to all of them.class NotificationManager implements INotifier { private listOfNotifiers: array<INotifier>; constructor(notifiers: array<INotifier>) { this.listOfNotifiers = notifiers; } function send(message) { foreach (notifier in this.listOfNotifiers) { notifier.send(message); } } } - Compose at Runtime: To send an alert via Email and SMS, you would create an instance of the
NotificationManagerlike this:highPriorityNotifier = new NotificationManager([ new EmailNotifier(), new SMSNotifier() ]); highPriorityNotifier.send("System critical!");
This design is flexible. To add a new combination, you just change the array of notifiers you pass to the
NotificationManagerconstructor. No new classes are needed. This is a practical example of the Composite design pattern, which we'll see more of soon. - Define an Abstraction: Keep a simple, clear interface.
Conclusion
Today we've explored one of the most important principles in modern object-oriented design. By favoring flexible "has a" relationships over rigid "is a" hierarchies, you can build systems that are easier to understand, test, and extend.
Key Takeaways:
- "Is A" vs. "Has A": Use inheritance for true "is a" specializations. Use composition to give objects behaviors and responsibilities.
- Inheritance is Rigid: It creates tight coupling, can lead to a combinatorial explosion of classes, and forces subclasses to inherit unwanted functionality.
- Composition is Flexible: It allows you to build complex objects from small, independent, and reusable components. You can add, remove, or swap behaviors at runtime.
- Interfaces are the Glue: Composition works best when combined with the Dependency Inversion Principle. Components implement shared interfaces, allowing the main object to remain decoupled from concrete implementations.
Preview of the Next Module:
You've now completed the SOLID principles and the "Composition over Inheritance" principle. These are the fundamental building blocks for creating sound architecture. In our next module, we will begin exploring Foundational Design Patterns. You will see that many of these patterns, such as Strategy, Decorator, and Composite, are simply well-established, recurring applications of the "Composition over Inheritance" principle. This lesson has given you the perfect foundation to understand not just what those patterns are, but why they work.