Object-oriented design comparison
Composition vs Inheritance in OOP
One of the most enduring debates in object-oriented programming. Both are mechanisms for code reuse, but they differ in coupling, flexibility, and maintainability. The principle "favour composition over inheritance" has been canonical since the Gang of Four book in 1994 — yet inheritance is still taught first everywhere.
TL;DR
- Inheritance (is-a) — a class extends another, inheriting behaviour. Simple to start with but creates tight coupling; deep hierarchies become fragile and hard to change.
- Composition (has-a) — a class contains other objects and delegates to them. More flexible, easier to test, and less brittle. The modern default.
- "Favour composition over inheritance." Use inheritance for genuine, stable is-a relationships. Use composition for behaviour reuse.
Side-by-side comparison
| Aspect | Inheritance | Composition |
|---|---|---|
| Relationship | is-a (Dog is an Animal) | has-a (Car has an Engine) |
| Coupling | Tight — subclass depends on base internals | Loose — depends only on interface/contract |
| Flexibility | Low — hierarchy fixed at compile time | High — swap components at runtime |
| Code reuse | Implicit — inherited automatically | Explicit — delegate calls to contained object |
| Deep hierarchy | Fragile base class problem; hard to change | No hierarchy; each class stays small |
| Testability | Harder — must instantiate full hierarchy | Easier — inject mock objects |
| Multiple behaviour | Limited by single inheritance (most languages) | Combine many components freely |
| Best for | Shallow type hierarchies, framework base classes | Complex behaviour built from small focused pieces |
Code side-by-side
A logger with different output targets:
Inheritance (tight coupling)
class Logger {
log(msg) { /* base logging */ }
}
class FileLogger extends Logger {
log(msg) {
super.log(msg); // fragile: base dependency
writeToFile(msg);
}
}
class CloudLogger extends FileLogger {
// Must extend FileLogger even if you
// only want cloud — forced hierarchy
log(msg) {
super.log(msg);
sendToCloud(msg);
}
} Composition (loose coupling)
class Logger {
constructor(transports) {
this.transports = transports; // injected
}
log(msg) {
this.transports.forEach(t => t.write(msg));
}
}
// Mix and match any transports:
const logger = new Logger([
new ConsoleTransport(),
new FileTransport('./app.log'),
new CloudTransport(API_KEY),
]);
// Swap or add transports without subclassing When to use Inheritance
- Genuine, stable is-a relationships.
ElectricCar extends CarorAdminUser extends User— the relationship is conceptually true and unlikely to change. - Framework base classes.
extends React.Component,extends BaseModel— the framework defines the contract; you override specific hooks. - Shallow hierarchies (max 2 levels). One or two levels of inheritance are usually fine; three or more becomes a maintenance problem.
- Polymorphism via a shared interface. When you need to treat all instances of a type uniformly (
Animal.speak()called on a Cat or a Dog), inheritance is the natural fit.
When to use Composition
- Behaviour from multiple sources. Most languages do not support multiple inheritance. Composition lets a class incorporate behaviours from as many sources as needed.
- Behaviour that needs to change at runtime. Inject a different strategy, transport, or formatter at construction time or even dynamically — inheritance cannot do this.
- Unit testing. Replace real dependencies with mocks by injecting them. Deeply inherited classes make mocking painful.
- Any time you reach for inheritance "just for code reuse". If the only reason to inherit is to share a method, extract that method into a collaborator and compose instead.
English phrases engineers use
Inheritance conversations
- "
AdminUserextendsUserand overrides the permissions check." - "We have a fragile base class — changing it breaks three subclasses."
- "The hierarchy is four levels deep — it's getting hard to follow."
- "Call super() first or the parent constructor won't run."
- "The Liskov Substitution Principle says subclasses must be replaceable for their base type."
Composition conversations
- "Favour composition over inheritance — inject the behaviour."
- "The class delegates to the payment processor; it doesn't know the implementation."
- "We inject the logger so we can mock it in tests."
- "This is the Strategy pattern — swap algorithms at runtime."
- "Has-a is more flexible than is-a here."
Quick decision tree
- Genuine is-a relationship, shallow hierarchy → Inheritance
- Reusing behaviour from multiple sources → Composition
- Need to swap behaviour at runtime → Composition
- Framework requires extending a base class → Inheritance (no choice)
- Deep hierarchy is forming (3+ levels) → Refactor to Composition
- Need to unit-test with mocks → Composition (inject dependencies)
- "I want to reuse this method" → Composition (not the right reason for inheritance)
Frequently asked questions
What is inheritance in OOP?
Inheritance (is-a relationship) lets a class extend another class, inheriting its properties and methods. A Dog class that extends Animal automatically has all of Animal's behaviour. It creates a type hierarchy. The extending class can override methods to customise behaviour.
What is composition in OOP?
Composition (has-a relationship) means a class contains instances of other classes rather than extending them. A Car class has an Engine, a Gearbox, and four Wheels — it does not extend Engine. Behaviour is delegated to the contained objects. This keeps each class small and focused.
What does "favour composition over inheritance" mean?
It is one of the most quoted principles from the Gang of Four Design Patterns book (1994). The advice is to prefer building complex behaviour by combining small, focused objects rather than building deep inheritance hierarchies. Deep hierarchies become fragile — changing a base class can break all subclasses unexpectedly.
When is inheritance the right choice?
Inheritance is appropriate when there is a genuine is-a relationship that will not change over time (a Cat is always an Animal), when you need polymorphism via a shared interface (treating all Animals uniformly), and when the hierarchy is shallow (1–2 levels). Framework base classes (e.g., React.Component, Django View) are also legitimate uses.
What is the fragile base class problem?
The fragile base class problem occurs when a change to a base class unexpectedly breaks subclasses. Because subclasses depend on the internal implementation of the base class (not just its interface), any change to the base class can ripple down in unpredictable ways. This is the main reason the OOP community drifted toward "favour composition".
How does composition relate to mixins and traits?
Mixins and traits (as in Ruby, Rust, Scala, PHP) are a middle ground — they allow sharing behaviour without the full is-a hierarchy of inheritance. Conceptually they are closer to composition: you compose a class from several small behaviours. Languages without multiple inheritance often use these patterns to get the benefits of composition with concise syntax.