Watch the video (10 min)
On this page23 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Is Composition?
  5. 2. A Car That Has an Engine
  6. 3. Delegation
  7. 4. The Fragile Hierarchy
  8. 5. Refactoring to Composition
  9. 6. Injecting Behaviour Through the Constructor
  10. 7. Changing Behaviour While the Program Runs
  11. 8. Adding a New Behaviour
  12. 9. Favor Composition over Inheritance
  13. 10. When Inheritance Is Right
  14. 11. A Real-World Example
  15. Code Examples
  16. Common Beginner Mistakes
  17. Best Practices
  18. Mini Challenge
  19. Practice Questions
  20. Practice Answers
  21. Quick Revision
  22. Key Takeaways
  23. What's Next?

Lecture 66 — Composition vs Inheritance

Category10 — Advanced OOP
Lecture066 of 140
Estimated video length10 min
TargetDart 3.12.2
LevelIntermediate

Inheritance says "a B is an A". Composition says "a B has an A". Building objects from smaller objects keeps your classes flexible when the requirements change.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2, so you will recognize it when you meet it yourself.


Prerequisites

Students should already understand, from Lectures 052–065:

  • classes, constructors, private members, and getters
  • extends, overriding, and super
  • abstract classes and interfaces with implements
  • polymorphism: one variable type, many object types

Connection With Previous Lectures

Lecture 059 gave us the is-a test and left one question open: "a car has an engine" — so what do we write instead of extends? Here is what happens if we ignore the test:

dart
class Engine {
  void start() {
    print('Engine running');
  }
}

class Car extends Engine {}

void main() {
  var car = Car();
  car.start();
  print(car is Engine);
}
output
Engine running
true

It compiles, but Dart now believes a car is an engine. And with only one superclass, where do the wheels and the radio go? This lecture shows the other way to reuse code.


Learning Objectives

By the end of this lecture you will be able to:

  • Explain composition as an object holding other objects
  • Use delegation to forward work to an inner object
  • Recognize a fragile class hierarchy and why it breaks
  • Refactor an inheritance tree into composition
  • Inject behaviour objects through the constructor using an interface
  • Swap an object's behaviour while the program runs
  • Apply the rule "favor composition over inheritance"
  • Decide when inheritance is still the right choice

1. What Is Composition?

Composition means building an object out of other objects that it keeps in its fields. The outer object has the inner ones.

Analogy — a car is assembled from parts: an engine, four wheels, a radio. A mechanic can replace the engine without rebuilding the car.


2. A Car That Has an Engine

The engine becomes a field:

dart
class Engine {
  void start() {
    print('Engine running');
  }
}

class Car {
  final Engine engine = Engine();

  void drive() {
    engine.start();
    print('Car moving');
  }
}

void main() {
  var car = Car();
  car.drive();
  print(car is Engine);
}
output
Engine running
Car moving
false

The car uses the engine's code, but is no longer an engine. It could hold a Radio and four Wheels too — a class can have as many fields as it needs.


3. Delegation

When drive() calls engine.start(), the car delegates: it hands the job to the object that knows how to do it. Delegation also lets you choose what to expose. Keep the part private and forward only the methods you want:

dart
class Battery {
  int level = 40;

  void charge(int amount) {
    level += amount;
  }
}

class Phone {
  final _battery = Battery();

  void plugIn() => _battery.charge(30);

  int get batteryLevel => _battery.level;
}

void main() {
  var phone = Phone();
  phone.plugIn();
  print('Battery: ${phone.batteryLevel}%');
}
output
Battery: 70%

Outside code can charge the phone and read the level, but cannot set level to 500. With extends Battery, every public Battery member would belong to Phone.


4. The Fragile Hierarchy

Inheritance works until the requirements change. A company starts with salaried staff, and managers earn a bonus:

dart
class Employee {
  Employee(this.name);
  final String name;
  double pay() => 0;
}

class SalariedEmployee extends Employee {
  SalariedEmployee(super.name, this.salary);
  final double salary;

  @override
  double pay() => salary;
}

class SalariedManager extends SalariedEmployee {
  SalariedManager(super.name, super.salary);

  @override
  double pay() => super.pay() + 500;
}

void main() {
  var sara = SalariedManager('Sara', 3000);
  print('${sara.name}: ${sara.pay()}');
}
output
Sara: 3500.0

Clean so far. Then three new requirements arrive:

  1. Hourly workers. We add HourlyEmployee and HourlyManager, which must copy the + 500 bonus — it cannot extend both HourlyEmployee and SalariedManager.
  2. Contractors. Every role needs a third version: ContractManager…
  3. Sara moves to hourly pay. An object can never change its class, so her SalariedManager must be thrown away and rebuilt.
output
Employee
├── SalariedEmployee
│   └── SalariedManager
├── HourlyEmployee
│   └── HourlyManager      ← bonus copied
└── ContractEmployee
    └── ContractManager    ← bonus copied again

Two kinds of pay × two roles needs 4 classes; three × three needs 9. The tree mixes two separate ideas — how someone is paid and what job they do — into one chain, so each change cracks it somewhere. This is a fragile hierarchy.


5. Refactoring to Composition

Pull the changing idea — how someone is paid — out into its own objects. First an interface (Lecture 063):

dart
abstract class PayPlan {
  double amount();
}

class SalaryPlan implements PayPlan {
  SalaryPlan(this.salary);
  final double salary;

  @override
  double amount() => salary;
}

class HourlyPlan implements PayPlan {
  HourlyPlan(this.hours, this.rate);
  final int hours;
  final double rate;

  @override
  double amount() => hours * rate;
}

Now Employee has a pay plan, and the manager bonus is just a value:

dart
class Employee {
  Employee(this.name, this.plan, {this.bonus = 0});
  final String name;
  final PayPlan plan;
  final double bonus;

  double pay() => plan.amount() + bonus;
}

void main() {
  var sara = Employee('Sara', SalaryPlan(3000), bonus: 500);
  var ali = Employee('Ali', HourlyPlan(80, 20));
  print('${sara.name}: ${sara.pay()}');
  print('${ali.name}: ${ali.pay()}');
}
output
Sara: 3500.0
Ali: 1600.0

(Run it with the PayPlan classes above.) One class replaces the tree; the bonus is written once.


6. Injecting Behaviour Through the Constructor

In Employee('Sara', SalaryPlan(3000)) the employee does not create its own plan — we pass one in. This is called injecting a behaviour object.

Employee only knows the PayPlan interface, so any class that implements it fits. That is polymorphism (Lecture 065) at work: plan.amount() runs whichever version the object has. Programmers call this design the strategy idea — the "how" is a separate object you plug in.


7. Changing Behaviour While the Program Runs

Remove final from the field and the plan can be replaced on the same object:

dart
class Employee {
  Employee(this.name, this.plan);
  final String name;
  PayPlan plan;

  double pay() => plan.amount();
}

void main() {
  var sara = Employee('Sara', SalaryPlan(3000));
  print(sara.pay());
  sara.plan = HourlyPlan(100, 25);
  print(sara.pay());
}
output
3000.0
2500.0

The third requirement from Section 4 — impossible with inheritance — is one assignment.


8. Adding a New Behaviour

Contractors? Write one new class. Employee does not change, and no manager variant is needed:

dart
class ContractPlan implements PayPlan {
  ContractPlan(this.projectFee);
  final double projectFee;

  @override
  double amount() => projectFee;
}

void main() {
  var zayan = Employee('Zayan', ContractPlan(5000));
  print('${zayan.name}: ${zayan.pay()}');
}
output
Zayan: 5000.0

9. Favor Composition over Inheritance

This rule of thumb is famous for a reason:

Inheritance (extends)Composition (a field)
Relationshipis-ahas-a
How manyone superclassany number of parts
Chosenwhen the class is writtenwhen the object is created
Can change laterneveryes, if the field is not final
Exposesevery public parent memberonly what you forward

When unsure, start with a field — untangling a deep tree later is painful.


10. When Inheritance Is Right

Inheritance is still the right tool when all of these are true:

  • the is-a sentence is true and will stay true ("a savings account is an account");
  • the child wants every parent member, not just a few;
  • the hierarchy is shallow — one or two levels;
  • the variation does not change while the program runs.

SavingsAccount extends Account from Lecture 061 passes all four. Mixins (Lecture 064) are a third option, for sharing a small set of methods across unrelated classes.


11. A Real-World Example

A shopping cart composed of items and a discount rule that can be swapped at checkout:

dart
abstract class Discount {
  double apply(double total);
}

class NoDiscount implements Discount {
  @override
  double apply(double total) => total;
}

class PercentOff implements Discount {
  PercentOff(this.percent);
  final double percent;

  @override
  double apply(double total) => total - total * percent / 100;
}

class Item {
  Item(this.name, this.price);
  final String name;
  final double price;
}

class Cart {
  Cart(this.discount);
  final _items = <Item>[];
  Discount discount;

  void add(Item item) => _items.add(item);

  double get total {
    var sum = 0.0;
    for (var item in _items) {
      sum += item.price;
    }
    return discount.apply(sum);
  }
}

void main() {
  var cart = Cart(NoDiscount());
  cart.add(Item('Bag', 1500));
  cart.add(Item('Pen', 500));
  print('Total: ${cart.total}');
  cart.discount = PercentOff(10);
  print('With coupon: ${cart.total}');
}
output
Total: 2000.0
With coupon: 1800.0

Cart has a private item list it delegates to, and a Discount injected through the constructor and swapped later.


Code Examples

Example 1 — Has-a with a Field

dart
class Wheel {
  void roll() => print('Wheel rolling');
}

class Bicycle {
  final front = Wheel();
  final back = Wheel();

  void ride() {
    front.roll();
    back.roll();
  }
}

void main() {
  Bicycle().ride();
}
output
Wheel rolling
Wheel rolling

Example 2 — Delegating a Method

dart
class Speaker {
  void play(String song) => print('Playing $song');
}

class Car {
  final _speaker = Speaker();

  void playMusic(String song) => _speaker.play(song);
}

void main() {
  Car().playMusic('Dil Dil Pakistan');
}
output
Playing Dil Dil Pakistan

Example 3 — Exposing Only What You Want

dart
class Stack {
  final _items = <int>[];

  void push(int value) => _items.add(value);

  int pop() => _items.removeLast();

  bool get isEmpty => _items.isEmpty;
}

void main() {
  var stack = Stack();
  stack.push(1);
  stack.push(2);
  print(stack.pop());
  print(stack.isEmpty);
}
output
2
false

The private list hides insert and other methods that would break the stack's rule.

Example 4 — Injecting a Behaviour

dart
abstract class Greeting {
  String hello(String name);
}

class English implements Greeting {
  @override
  String hello(String name) => 'Hello, $name';
}

class Urdu implements Greeting {
  @override
  String hello(String name) => 'Assalam-o-Alaikum, $name';
}

class Greeter {
  Greeter(this.greeting);
  final Greeting greeting;

  void greet(String name) => print(greeting.hello(name));
}

void main() {
  Greeter(English()).greet('Ali');
  Greeter(Urdu()).greet('Sara');
}
output
Hello, Ali
Assalam-o-Alaikum, Sara

Example 5 — Swapping at Runtime

dart
abstract class Weapon {
  int damage();
}

class Sword implements Weapon {
  @override
  int damage() => 10;
}

class Bow implements Weapon {
  @override
  int damage() => 7;
}

class Player {
  Player(this.weapon);
  Weapon weapon;

  void attack() => print('Hit for ${weapon.damage()}');
}

void main() {
  var zayan = Player(Sword());
  zayan.attack();
  zayan.weapon = Bow();
  zayan.attack();
}
output
Hit for 10
Hit for 7

Example 6 — Student Marks

dart
abstract class GradingRule {
  String grade(int marks);
}

class PassFail implements GradingRule {
  @override
  String grade(int marks) => marks >= 50 ? 'Pass' : 'Fail';
}

class Letter implements GradingRule {
  @override
  String grade(int marks) => marks >= 80 ? 'A' : 'B';
}

class Student {
  Student(this.name, this.marks, this.rule);
  final String name;
  final int marks;
  final GradingRule rule;

  @override
  String toString() => '$name: ${rule.grade(marks)}';
}

void main() {
  print(Student('Ali', 85, Letter()));
  print(Student('Sara', 45, PassFail()));
}
output
Ali: A
Sara: Fail

Example 7 — Bank Account with a Fee Policy

dart
abstract class FeePolicy {
  double fee(double amount);
}

class StandardFee implements FeePolicy {
  @override
  double fee(double amount) => amount * 0.02;
}

class NoFee implements FeePolicy {
  @override
  double fee(double amount) => 0;
}

class Account {
  Account(this.owner, this.balance, this.policy);
  final String owner;
  double balance;
  FeePolicy policy;

  void withdraw(double amount) {
    balance -= amount + policy.fee(amount);
  }
}

void main() {
  var ali = Account('Ali', 1000, StandardFee());
  ali.withdraw(500);
  print(ali.balance);
  ali.policy = NoFee();
  ali.withdraw(100);
  print(ali.balance);
}
output
490.0
390.0

Example 8 — Temperature Display

dart
abstract class Formatter {
  String format(double celsius);
}

class CelsiusFormat implements Formatter {
  @override
  String format(double celsius) => '$celsius°C';
}

class FahrenheitFormat implements Formatter {
  @override
  String format(double celsius) => '${celsius * 9 / 5 + 32}°F';
}

class Thermometer {
  Thermometer(this.formatter);
  Formatter formatter;
  double reading = 25.0;

  void show() => print(formatter.format(reading));
}

void main() {
  var t = Thermometer(CelsiusFormat());
  t.show();
  t.formatter = FahrenheitFormat();
  t.show();
}
output
25.0°C
77.0°F

Common Beginner Mistakes

1. Using extends for a has-a relationship

class Car extends Engine compiles, so Dart cannot warn you. If "a B is an A" sounds wrong, make A a field.

2. Forgetting to give the part a value

dart
class Car {
  // Engine engine; // ✗ no value, and no constructor sets it
  final Engine engine = Engine(); // ✓
}
output
Error: Field 'engine' should be initialized because its type 'Engine' doesn't allow null.

A part is a normal non-nullable field: set it where it is declared or in the constructor.

3. Typing the field with a concrete class

dart
class Employee {
  Employee(this.plan);
  final SalaryPlan plan; // ✗ should be PayPlan
}

void main() {
  // Employee(HourlyPlan(80, 20)); // ✗
}
output
Error: The argument type 'HourlyPlan' can't be assigned to the parameter type 'SalaryPlan'.

Type the field with the interface, so every implementation fits.

4. Making the behaviour final, then swapping it

dart
class Player {
  Player(this.weapon);
  final Weapon weapon;
}

void main() {
  var zayan = Player(Sword());
  // zayan.weapon = Bow(); // ✗
}
output
Error: The setter 'weapon' isn't defined for the type 'Player'.

Remove final when the behaviour must be swappable.


Best Practices

  • Apply the is-a test before every extends; use a field for has-a.
  • Put the idea that changes most into its own interface and classes.
  • Type injected fields with the interface, not a concrete class.
  • Pass behaviour objects in through the constructor instead of creating them inside.
  • Keep inheritance shallow and save it for true, stable is-a relationships.

Mini Challenge

Create an interface Notifier with void send(String message), and two classes: EmailNotifier prints Email: <message>, and SmsNotifier prints SMS: <message>. Create an Order that receives a Notifier through its constructor and has place(String item), which sends Order placed: <item>. Place an order for a Bag by email, switch the notifier to SMS, and place an order for a Pen.

Your program should print exactly:

output
Email: Order placed: Bag
SMS: Order placed: Pen

Try it yourself before opening the solution.

Show solution
dart
abstract class Notifier {
  void send(String message);
}

class EmailNotifier implements Notifier {
  @override
  void send(String message) => print('Email: $message');
}

class SmsNotifier implements Notifier {
  @override
  void send(String message) => print('SMS: $message');
}

class Order {
  Order(this.notifier);
  Notifier notifier;

  void place(String item) => notifier.send('Order placed: $item');
}

void main() {
  var order = Order(EmailNotifier());
  order.place('Bag');
  order.notifier = SmsNotifier();
  order.place('Pen');
}
output
Email: Order placed: Bag
SMS: Order placed: Pen

Practice Questions

Work through these in order — they run from beginner to application to challenge.

Beginner

  1. Create Engine with start() printing Vroom. Create Car that has an engine and a drive() method that starts it, then prints Driving.
  2. For each pair, say "is-a" or "has-a": Student / Person, House / Room, Laptop / Battery, Dog / Animal.
  3. Create Library with a private List<String> of books, addBook(String title), and a getter count. Add two books and print 2.

Application

  1. Create an interface Shape with double area(), and Square and Circle (use 3.14). Create Room that has a Shape floor and a getter floorArea. Print the area of a Room(Square(4)).
  2. Create an interface Sound with String make(), and Bark and Meow. Create Pet that receives a Sound and has speak() printing it. Make one dog and one cat.
  3. Using the Player and Weapon classes from Example 5, add a Hammer with damage 15, and switch Zayan to it after his first attack.
  4. Without running it: in Section 4, why can't HourlyManager reuse the bonus code from SalariedManager?

Challenge

  1. Create an interface TaxRule with double tax(double price). Write NoTax and SalesTax (17%). A Product has a name, a price and a TaxRule, and a getter finalPrice. Print Book: 1000.0 and Phone: 11700.0.
  2. A school has Teacher, PartTimeTeacher, Principal and PartTimePrincipal. Redesign it with composition so it needs one Staff class. Print Sara: 4500.0 (full-time 4000 plus principal bonus 500) and Ali: 1200.0 (part-time, 60 hours at 20).

Practice Answers

Question 1 — Solution

dart
class Engine {
  void start() => print('Vroom');
}

class Car {
  final engine = Engine();

  void drive() {
    engine.start();
    print('Driving');
  }
}

void main() {
  Car().drive();
}
output
Vroom
Driving

Question 2 — Solution

A student is a person; a house has rooms; a laptop has a battery; a dog is an animal.

Question 3 — Solution

dart
class Library {
  final _books = <String>[];

  void addBook(String title) => _books.add(title);

  int get count => _books.length;
}

void main() {
  var library = Library();
  library.addBook('Dart Basics');
  library.addBook('Clean Code');
  print(library.count);
}
output
2

Question 4 — Solution

dart
abstract class Shape {
  double area();
}

class Square implements Shape {
  Square(this.side);
  final double side;

  @override
  double area() => side * side;
}

class Circle implements Shape {
  Circle(this.radius);
  final double radius;

  @override
  double area() => 3.14 * radius * radius;
}

class Room {
  Room(this.floor);
  final Shape floor;

  double get floorArea => floor.area();
}

void main() {
  print(Room(Square(4)).floorArea);
}
output
16.0

Question 5 — Solution

dart
abstract class Sound {
  String make();
}

class Bark implements Sound {
  @override
  String make() => 'Woof';
}

class Meow implements Sound {
  @override
  String make() => 'Meow';
}

class Pet {
  Pet(this.sound);
  final Sound sound;

  void speak() => print(sound.make());
}

void main() {
  Pet(Bark()).speak();
  Pet(Meow()).speak();
}
output
Woof
Meow

Question 6 — Solution

Add this class to Example 5 and change main:

dart
class Hammer implements Weapon {
  @override
  int damage() => 15;
}

void main() {
  var zayan = Player(Sword());
  zayan.attack();
  zayan.weapon = Hammer();
  zayan.attack();
}
output
Hit for 10
Hit for 15

Question 7 — Solution

A Dart class has one superclass. HourlyManager extends HourlyEmployee for hourly pay, so it cannot also extend SalariedManager — the bonus must be copied.

Question 8 — Solution

dart
abstract class TaxRule {
  double tax(double price);
}

class NoTax implements TaxRule {
  @override
  double tax(double price) => 0;
}

class SalesTax implements TaxRule {
  @override
  double tax(double price) => price * 0.17;
}

class Product {
  Product(this.name, this.price, this.rule);
  final String name;
  final double price;
  final TaxRule rule;

  double get finalPrice => price + rule.tax(price);
}

void main() {
  var products = [
    Product('Book', 1000, NoTax()),
    Product('Phone', 10000, SalesTax()),
  ];
  for (var p in products) {
    print('${p.name}: ${p.finalPrice}');
  }
}
output
Book: 1000.0
Phone: 11700.0

Question 9 — Solution

dart
abstract class PayPlan {
  double amount();
}

class FullTime implements PayPlan {
  FullTime(this.salary);
  final double salary;

  @override
  double amount() => salary;
}

class PartTime implements PayPlan {
  PartTime(this.hours, this.rate);
  final int hours;
  final double rate;

  @override
  double amount() => hours * rate;
}

class Staff {
  Staff(this.name, this.plan, {this.bonus = 0});
  final String name;
  final PayPlan plan;
  final double bonus;

  double pay() => plan.amount() + bonus;
}

void main() {
  var sara = Staff('Sara', FullTime(4000), bonus: 500);
  var ali = Staff('Ali', PartTime(60, 20));
  print('${sara.name}: ${sara.pay()}');
  print('${ali.name}: ${ali.pay()}');
}
output
Sara: 4500.0
Ali: 1200.0

Full-time or part-time is the plan; teacher or principal is the bonus.


Quick Revision

  1. What is composition?
  2. What does delegation mean?
  3. Why does a deep hierarchy break when a second kind of variation appears?
  4. What does it mean to inject a behaviour object?
  5. Why should an injected field use the interface type?
  6. How do you let an object change its behaviour while the program runs?
  7. When is inheritance still the right choice?

Key Takeaways

  • Composition builds an object from other objects held in its fields — a has-a relationship.
  • Delegation forwards work to an inner object and exposes only what you choose.
  • A deep tree that mixes two ideas multiplies classes, copies code, and cannot change an object's class later — a fragile hierarchy.
  • Refactor by moving the changing idea into an interface with small implementing classes.
  • Inject behaviour through the constructor and type the field with the interface.
  • A non-final behaviour field can be swapped while the program runs.
  • Favor composition over inheritance; use extends for true, stable, shallow is-a relationships.

What's Next?

Lecture 67 — Enums. That closes Advanced OOP. Many values come from a small fixed set — a pay type, an order status, a weekday — and so far we have stored them as strings that a typo can break. Next we will learn how enums give such a set its own type, so Dart checks every value for us.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.