Menu
Coddy logo textTech

Decorator Pattern

Part of the Object Oriented Programming section of Coddy's Dart journey — lesson 96 of 110.

The Decorator pattern lets you add new behaviors to objects dynamically by wrapping them in special objects called decorators. Unlike inheritance, which adds behavior at compile time, decorators attach additional responsibilities at runtime without modifying the original class.

The key idea is that both the original object and decorators implement the same interface.

A decorator wraps the original object, delegates to it, and adds its own behavior before or after:

abstract class Coffee {
  String getDescription();
  double getCost();
}

class SimpleCoffee implements Coffee {
  @override
  String getDescription() => 'Simple Coffee';

  @override
  double getCost() => 2.0;
}

class MilkDecorator implements Coffee {
  final Coffee _coffee;
  MilkDecorator(this._coffee);

  @override
  String getDescription() => '${_coffee.getDescription()}, Milk';

  @override
  double getCost() => _coffee.getCost() + 0.5;
}

class SugarDecorator implements Coffee {
  final Coffee _coffee;
  SugarDecorator(this._coffee);

  @override
  String getDescription() => '${_coffee.getDescription()}, Sugar';

  @override
  double getCost() => _coffee.getCost() + 0.2;
}

void main() {
  Coffee coffee = SimpleCoffee();
  coffee = MilkDecorator(coffee);
  coffee = SugarDecorator(coffee);

  print(coffee.getDescription());  // Simple Coffee, Milk, Sugar
  print(coffee.getCost());         // 2.7
}

Each decorator wraps the previous object and enhances it. You can stack multiple decorators in any combination, creating flexible behavior without creating a subclass for every possible combination. This pattern is ideal when you need to add features to objects without affecting other objects of the same class.

challenge icon

Challenge

Easy

Let's build a pizza ordering system using the Decorator pattern! You'll create a base pizza that can be enhanced with various toppings, where each topping wraps the previous item and adds to both the description and the price.

You'll organize your code into two files:

  • pizza.dart: This file contains your pizza interface and all the decorator classes. Start with an abstract class Pizza that defines two methods: String getDescription() and double getPrice(). Then create a PlainPizza class that implements Pizza - it returns Plain Pizza as its description and costs 8.0. Now build three decorator classes that each wrap a Pizza and enhance it:
    • CheeseDecorator - adds , Cheese to the description and 1.5 to the price
    • PepperoniDecorator - adds , Pepperoni to the description and 2.0 to the price
    • MushroomDecorator - adds , Mushrooms to the description and 1.25 to the price
    Each decorator should implement Pizza, accept a Pizza in its constructor, and delegate to the wrapped pizza before adding its own contribution.
  • main.dart: Import your pizza file and demonstrate how decorators stack to build a customized order. Start with a PlainPizza, then wrap it with CheeseDecorator, then wrap that with PepperoniDecorator, and finally wrap everything with MushroomDecorator. Print the final pizza's description, then print Total: $ followed by the price.

Notice how each decorator builds upon the previous one - you're creating a fully loaded pizza by stacking simple, single-responsibility decorators!

Expected output:

Plain Pizza, Cheese, Pepperoni, Mushrooms
Total: $12.75

Cheat sheet

The Decorator pattern adds new behaviors to objects dynamically by wrapping them in decorator objects. Both the original object and decorators implement the same interface.

Key components:

  • An abstract class or interface that defines the common methods
  • A concrete class that implements the base functionality
  • Decorator classes that wrap the original object and enhance it

Each decorator:

  • Implements the same interface as the object it decorates
  • Holds a reference to the wrapped object
  • Delegates calls to the wrapped object
  • Adds its own behavior before or after delegation
abstract class Coffee {
  String getDescription();
  double getCost();
}

class SimpleCoffee implements Coffee {
  @override
  String getDescription() => 'Simple Coffee';

  @override
  double getCost() => 2.0;
}

class MilkDecorator implements Coffee {
  final Coffee _coffee;
  MilkDecorator(this._coffee);

  @override
  String getDescription() => '${_coffee.getDescription()}, Milk';

  @override
  double getCost() => _coffee.getCost() + 0.5;
}

// Stack multiple decorators
Coffee coffee = SimpleCoffee();
coffee = MilkDecorator(coffee);
coffee = SugarDecorator(coffee);

Decorators can be stacked in any combination, creating flexible behavior without creating subclasses for every possible combination.

Try it yourself

import 'pizza.dart';

void main() {
  // TODO: Create a PlainPizza
  
  // TODO: Wrap it with CheeseDecorator
  
  // TODO: Wrap that with PepperoniDecorator
  
  // TODO: Wrap everything with MushroomDecorator
  
  // TODO: Print the final pizza's description
  
  // TODO: Print "Total: $" followed by the price
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming