Hello! Welcome to your next lesson in the Fundamental Design Principles module.
In our previous lesson, we established that high cohesion and low coupling are the twin pillars of maintainable software design. We saw that tight coupling, where classes are heavily dependent on each other's internal details, makes systems fragile and hard to change. Our goal is to minimize these dependencies.
Today, we'll explore one of the most powerful and widely cited principles for achieving low coupling: "Prefer composition over inheritance." This isn't just a catchy phrase; it's a practical design guideline that directly impacts the flexibility and reusability of your code. Mastering this principle is a significant step toward thinking like a system designer, a skill essential for your interview preparation.
This lesson directly addresses the learning outcome: Apply the 'composition over inheritance' principle to create flexible and reusable designs. We'll break down the trade-offs between these two code-reuse mechanisms and see how choosing composition often leads to more robust and adaptable systems.
1. The Trouble with Inheritance
Inheritance is often the first code-reuse mechanism we learn in object-oriented programming. It models an "is-a" relationship (a Dog is an Animal). While powerful, it has some significant downsides that can lead to rigid and fragile designs.
The main problems with inheritance are:
- Tight Coupling: A subclass is intrinsically tied to its superclass's implementation. A change in the superclass, even a seemingly safe one, can break its subclasses. This is known as the Fragile Base Class Problem.
- Inflexibility: In Java, a class can only extend one other class. This can be very limiting. What if you need to inherit behavior from two different, unrelated classes? This often leads to deep and complex inheritance hierarchies that are difficult to understand and maintain.
- Unwanted Functionality: A subclass inherits all public and protected members of its parent, even ones that might not make sense for it. This can violate the principle of least astonishment and lead to awkward workarounds, like overriding a method just to throw an
UnsupportedOperationException.
To see these issues in a practical context, let's watch a segment from the following video.
The video 'The Flaws of Inheritance' by CodeAesthetic uses an excellent image processing example to demonstrate how a seemingly logical inheritance structure can become problematic when new requirements are introduced.
Watch from the beginning to 03:28. Pay close attention to how the initial design, where JpgImage inherits from Image, runs into trouble when a DrawableImage is needed. Notice how the parent class's structure is 'thrust upon the child'.
As the video illustrates, the desire to reuse the resize and flip methods forces DrawableImage to inherit load and save methods that are completely irrelevant to it. This is a classic example of inheritance creating a design that's hard to adapt.
2. A More Flexible Alternative: Composition
Composition offers a different approach to code reuse. Instead of an "is-a" relationship, it models a "has-a" relationship. A class achieves its functionality not by being another thing, but by having other things. A Car has an Engine; it isn't an Engine.
This simple shift in perspective has profound benefits:
- Loose Coupling: The containing class interacts with the composed object through its public interface, not its internal implementation. You can swap out the component object with another one that has the same interface without affecting the container.
- Flexibility: A class can be composed of many other objects, allowing you to mix and match functionalities as needed. You are not limited by single inheritance.
- Clear Responsibilities: It encourages you to build complex objects out of smaller, single-responsibility components, which naturally leads to a more cohesive and understandable design.
Let's see how composition solves the problem from our previous example.
Now, let's continue with the same video to see how the image processing system is refactored using composition.
Watch from 03:28 to 05:14. Observe how the Image class is separated from the file format classes (JpgImage, PngImage). Instead of inheriting from Image, these classes now simply use an Image object. This decouples the representation of an image in memory from how it's saved or loaded.
The refactored design is far more flexible. The user of the code can now combine behaviors: load a JPEG, draw on it, and then save it. This was not easily achievable with the rigid inheritance hierarchy.
For a more detailed breakdown of the pros and cons of both approaches, the article "Composition Over Inheritance in System Design" is an excellent supplementary read.
Composition Over Inheritance in System Design - Medium
This article from Medium provides a comprehensive comparison. It clearly defines both concepts and lists their advantages and disadvantages.
Read the sections 'Inheritance', 'Composition', and 'Comparison: Composition vs. Inheritance'. This will solidify your understanding of the core trade-offs between the two principles regarding flexibility, maintenance, and use cases.
3. Deciding When to Use Which
The principle is "prefer" composition, not "always use" composition. Inheritance still has its place. The key is to know when each is appropriate. A very clear and disciplined rule of thumb is provided in the following video.
Only Use Inheritance If You Want Both of These
Christopher Okhravi's video 'Only Use Inheritance If You Want Both of These' offers a precise guideline for making this design choice.
Watch from the beginning to 07:32. This is a critical segment. Focus on his central argument: If you only need code reuse, use composition (0:17). If you only need subtype polymorphism (treating different objects the same way), use an interface (3:09). Inheritance is only a candidate when you need both hierarchical code reuse and subtype polymorphism (5:30). He also makes the compelling point that even when both conditions are met, a combination of an interface (for polymorphism) and composition (for code reuse) is often a superior, more flexible solution.
This gives us a powerful mental model:
- Want to reuse code? Think composition first.
- Want to treat different types of objects uniformly? Think interfaces.
- Want both, and the relationship is a genuine "is-a" specialization? Then, and only then, consider inheritance.
4. Refactoring from Inheritance to Composition in Java
Let's walk through a concrete Java example of how to replace inheritance with composition. This process typically involves a technique called delegation.
The basic steps are:
- Change the subclass so it no longer
extendsthe superclass. Instead, it shouldimplementthe same interface as the superclass (if one exists, or create one if needed). - Add a private field to the class to hold an instance of the object that provides the base functionality.
- In the constructor, initialize this field. This is a form of dependency injection.
- Implement the interface methods by "delegating" the calls to the wrapped object. You can then add the new or modified behavior before or after the delegation.
Replacing Inheritance with Composition
The article 'Replacing Inheritance with Composition' on JavaCodeGeeks provides a clear, hands-on Java example of this refactoring process.
Read the sections 'Basic Composition: The Delegate Pattern', 'Replacing the Template Pattern', and 'The Gains'. In 'Basic Composition', focus on how ComplexFoo is changed from extends BasicFoo to implementing the Foo interface and holding a Foo instance. 'Replacing the Template Pattern' shows a more advanced use case where the Strategy pattern (a composition-based pattern) replaces the inheritance-based Template Method pattern. 'The Gains' summarizes the key benefits, like avoiding 'combination hell' and improving testability.
This pattern of wrapping an object to add functionality is very common and powerful. It's the basis for several design patterns, most notably the Decorator pattern, which we will study later in the course.
Test your understanding!
Imagine you're designing a notification system. You start with an inheritance-based approach:
// Base class with common functionality
public abstract class Notifier {
public void send(String message) {
// Common logic, e.g., logging the message
System.out.println("Logging message: " + message);
// Delegate to specific implementation
sendMessage(message);
}
protected abstract void sendMessage(String message);
}
// Concrete implementations
public class EmailNotifier extends Notifier {
@Override
protected void sendMessage(String message) {
System.out.println("Sending Email: " + message);
}
}
public class SmsNotifier extends Notifier {
@Override
protected void sendMessage(String message) {
System.out.println("Sending SMS: " + message);
}
}
Now, a new requirement comes in: "We need to be able to send encrypted notifications."
How would you implement an EncryptedEmailNotifier using inheritance? What problems do you see with this approach, especially if you also need an EncryptedSmsNotifier? How would you redesign this using composition?
Show answer
1. Inheritance-based approach and its problems:
With inheritance, you'd likely create a new class that extends EmailNotifier:
public class EncryptedEmailNotifier extends EmailNotifier {
@Override
protected void sendMessage(String message) {
String encryptedMessage = "ENCRYPTED(" + message + ")";
super.sendMessage(encryptedMessage); // Calls the parent EmailNotifier's method
}
}
Problems:
- Combination Hell: This seems okay at first, but what about an
EncryptedSmsNotifier? You'd need another class,EncryptedSmsNotifier extends SmsNotifier. What if a third requirement comes in, like "send notifications with high priority"? You'd end up with an explosion of classes:HighPriorityEmailNotifier,HighPriorityEncryptedEmailNotifier,HighPrioritySmsNotifier, etc. The class hierarchy becomes unmanageable. - Rigidity: You are combining concerns (notification channel and encryption) in a rigid hierarchy. You can't dynamically decide to encrypt a message at runtime.
2. Redesign using Composition:
A better approach is to model these functionalities as separate, composable responsibilities.
// 1. Define a clear interface for the core responsibility.
public interface Notifier {
void send(String message);
}
// 2. Create concrete implementations for each channel.
public class EmailNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("Sending Email: " + message);
}
}
public class SmsNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("Sending SMS: " + message);
}
}
// 3. Create a "Decorator" using composition to add encryption.
public class EncryptedNotifier implements Notifier {
private final Notifier wrappedNotifier;
public EncryptedNotifier(Notifier notifier) {
this.wrappedNotifier = notifier;
}
@Override
public void send(String message) {
String encryptedMessage = "ENCRYPTED(" + message + ")";
// Delegate the call to the wrapped notifier
wrappedNotifier.send(encryptedMessage);
}
}
// Usage:
public class Main {
public static void main(String[] args) {
Notifier emailNotifier = new EmailNotifier();
Notifier smsNotifier = new SmsNotifier();
// Send a plain email
emailNotifier.send("Hello World!");
// Send an encrypted email by composing!
Notifier encryptedEmail = new EncryptedNotifier(emailNotifier);
encryptedEmail.send("This is a secret.");
// Send an encrypted SMS by composing!
Notifier encryptedSms = new EncryptedNotifier(smsNotifier);
encryptedSms.send("This is also a secret.");
}
}
This composition-based design is far more flexible. We can wrap any Notifier with the EncryptedNotifier to add encryption dynamically. If we need a HighPriorityNotifier, we can create another similar wrapper. We are combining behaviors at runtime by composing objects, not creating a rigid web of classes at compile time.
Conclusion
We've seen that while inheritance is a fundamental OOP concept, it creates tight coupling that can make your software rigid and fragile. By favoring composition, you can build systems from interchangeable parts, leading to designs that are more flexible, maintainable, and easier to test.
This UML diagram for a Parking Lot system—a classic LLD interview problem you'll tackle later—shows this principle in action.

Key Takeaways:
- Inheritance ("is-a"): Creates tight coupling between parent and child classes. Best reserved for true specialization hierarchies where subtype polymorphism is essential.
- Composition ("has-a"): Creates loose coupling by allowing objects to collaborate. It provides greater flexibility to mix and match functionalities.
- Flexibility vs. Rigidity: Composition lets you assemble behaviors at runtime, avoiding the "combinatorial explosion" of classes that can happen with deep inheritance hierarchies.
- Rule of Thumb: Prefer composition over inheritance as your default choice for code reuse. Challenge yourself to model relationships with "has-a" before falling back on "is-a".
In our next lesson, we will learn about the Law of Demeter, also known as the Principle of Least Knowledge. Now that we are building systems by composing objects, this principle will give us guidelines on how these objects should talk to each other to maintain the low coupling we've worked to achieve.