Lesson 23 min
Inheritance vs Composition
00:00 / 00:00
The Core Debate
Inheritance (extends) and Composition (holding references to collaborators) are two ways to reuse behavior. The industry maxim is: "Favour composition over inheritance." Dart supports both — knowing when to use each is a mark of a senior engineer.
Inheritance models an is-a relationship. Composition models a has-a relationship. Real software systems have far more has-a relationships than is-a ones.
Inheritance with extends
dartabstract class Animal { final String name; Animal(this.name); // Abstract method — subclasses MUST implement String speak(); // Concrete method — subclasses CAN override void describe() => print('$name says: ${speak()}'); } class Dog extends Animal { Dog(String name) : super(name); String speak() => 'Woof!'; // Extending behavior void fetch() => print('$name fetches the ball!'); } class Cat extends Animal { Cat(String name) : super(name); String speak() => 'Meow!'; } void makeAnimalSpeak(Animal animal) => animal.describe(); makeAnimalSpeak(Dog('Rex')); // Rex says: Woof! makeAnimalSpeak(Cat('Luna')); // Luna says: Meow!
The Problem with Deep Inheritance
dart// ANTI-PATTERN: Deep inheritance hierarchy class Vehicle { } class MotorVehicle extends Vehicle { } class Car extends MotorVehicle { } class ElectricCar extends Car { } class TeslaModelS extends ElectricCar { } // Fragile! // Changing Car forces you to reason about the entire chain below it
Composition — The Better Way
dart// Small, focused behaviors as separate classes class Engine { final int horsepower; Engine(this.horsepower); void start() => print('Engine started ($horsepower HP)'); } class Battery { final int capacityKwh; Battery(this.capacityKwh); double chargeLevel = 1.0; } class NavigationSystem { void navigate(String destination) => print('Navigating to $destination'); } // Composed class — owns instances of collaborators class ElectricCar { final String model; final Engine engine; final Battery battery; final NavigationSystem navigation; ElectricCar({ required this.model, required this.engine, required this.battery, required this.navigation, }); void drive(String destination) { engine.start(); navigation.navigate(destination); } } final tesla = ElectricCar( model: 'Model S', engine: Engine(670), battery: Battery(100), navigation: NavigationSystem(), ); tesla.drive('JavaShark HQ');
When to Use Each
- Use inheritance when there is a genuine, stable
is-arelationship (e.g.,DogIS-AAnimal) - Use inheritance for polymorphism with a small, controlled hierarchy
- Use composition when sharing behavior across unrelated classes
- Use composition when the relationship may change or evolve
- Use composition as the default when unsure
Summary
Composition produces systems that are more flexible, testable, and easier to reason about. Inheritance is a powerful tool, but a blunt one. Reserve it for genuine taxonomic relationships and keep hierarchies shallow (maximum 2-3 levels deep).