Composition and inheritance both reuse behavior, but they create different dependencies.
The practical question is not which one is more modern. It is whether the new type is a stable subtype, or whether another object should own one part of its work.
When inheritance fits
Inheritance models an “is-a” relationship. A profile screen is still a screen.
class Screen {
func showTitle() {
print("Title")
}
}
final class ProfileScreen: Screen {
override func showTitle() {
print("Profile")
}
}
This is reasonable when the base type has a stable contract and subclasses are valid substitutes for it.
Where inheritance gets expensive
The problem starts when a type needs several independent behaviors. Inheritance gives you one parent class, but it does not give you independent building blocks. Adding every new behavior to the same hierarchy makes the subclasses harder to name, test, and maintain.
Overriding a parent method can change the behavior, but the subclass still depends on the parent’s implementation and lifecycle. A base-class change can affect every subclass, and each subclass must understand which parent behavior it keeps or replaces.
Composition keeps behavior in separate objects
Composition means that an object owns smaller collaborators and delegates part of its work to them.
struct Engine {
func start() {
print("Engine started")
}
}
struct Car {
let engine: Engine
func start() {
engine.start()
}
}
Car does not inherit from Engine. It has an Engine and delegates the start operation to it. The engine logic is separate from Car, so it can be tested independently.
Quick choice
- Use inheritance when the relationship is a stable “is-a” relationship.
- Use composition when one object owns another object with a separate job.
- If a class grows unrelated responsibilities, move each responsibility into its own collaborator.
The simplest rule is this: inherit for a stable type identity, compose when another object should own a separate job.