Decorator Pattern
Part of the Object Oriented Programming section of Coddy's C# journey — lesson 58 of 70.
The Decorator pattern is a structural pattern that lets you attach new behaviors to objects by wrapping them in special objects called decorators. Unlike inheritance, which adds behavior at compile time, decorators add functionality dynamically at runtime without modifying the original class.
The pattern works by having decorators implement the same interface as the object they wrap. Each decorator holds a reference to the wrapped object and delegates calls to it, adding its own behavior before or after:
public interface ICoffee
{
string GetDescription();
decimal GetCost();
}
public class SimpleCoffee : ICoffee
{
public string GetDescription() => "Coffee";
public decimal GetCost() => 2.00m;
}
public abstract class CoffeeDecorator : ICoffee
{
protected ICoffee _coffee;
public CoffeeDecorator(ICoffee coffee) => _coffee = coffee;
public virtual string GetDescription() => _coffee.GetDescription();
public virtual decimal GetCost() => _coffee.GetCost();
}
public class MilkDecorator : CoffeeDecorator
{
public MilkDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", Milk";
public override decimal GetCost() => _coffee.GetCost() + 0.50m;
}The power of decorators lies in their ability to stack. You can wrap a decorated object with another decorator:
ICoffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new MilkDecorator(coffee); // Double milk!
Console.WriteLine(coffee.GetDescription()); // Coffee, Milk, Milk
Console.WriteLine(coffee.GetCost()); // 3.00The Decorator pattern is ideal when you need to add responsibilities to objects without creating an explosion of subclasses. Each decorator focuses on a single concern, and you combine them as needed to build the exact behavior you want.
Challenge
EasyLet's build a pizza ordering system using the Decorator pattern. You'll create a base pizza and then dynamically add toppings to it, with each topping wrapping the previous layer and adding to both the description and the price.
You'll organize your code across three files:
Pizza.cs: Define anIPizzainterface in thePizzaShopnamespace with two methods:GetDescription()returning a string andGetPrice()returning a decimal. Then create aPlainPizzaclass that implements this interface - it should return"Pizza"as its description and8.00mas its base price.ToppingDecorators.cs: Create an abstractToppingDecoratorclass that implementsIPizzaand holds a reference to the wrapped pizza (passed through the constructor). Its virtual methods should delegate to the wrapped pizza. Then create three concrete decorators:CheeseTopping- appends", Cheese"to the description and adds1.50mto the pricePepperoniTopping- appends", Pepperoni"to the description and adds2.00mto the priceMushroomTopping- appends", Mushrooms"to the description and adds1.25mto the price
Program.cs: Bring everything together by starting with a plain pizza and wrapping it with toppings based on input. Each topping decorator wraps the previous pizza, building up the final order layer by layer.
You will receive one input:
- A comma-separated list of toppings to add (e.g.,
cheese,pepperoni,cheese)
Start with a PlainPizza, then wrap it with the appropriate decorator for each topping in the order they appear. Valid toppings are cheese, pepperoni, and mushrooms. After applying all toppings, print the description on one line and the total price on the next line.
For example, if the input is cheese,pepperoni,mushrooms, the output should be:
Pizza, Cheese, Pepperoni, Mushrooms
12.75Notice how you can add the same topping multiple times! If the input is cheese,cheese, you'd get:
Pizza, Cheese, Cheese
11This demonstrates the decorator pattern's flexibility - each decorator wraps whatever pizza it receives, whether that's a plain pizza or an already-decorated one, allowing you to stack behaviors dynamically!
Cheat sheet
The Decorator pattern is a structural pattern that attaches new behaviors to objects by wrapping them in special decorator objects. It adds functionality dynamically at runtime without modifying the original class.
Decorators implement the same interface as the object they wrap, hold a reference to the wrapped object, and delegate calls to it while adding their own behavior:
public interface ICoffee
{
string GetDescription();
decimal GetCost();
}
public class SimpleCoffee : ICoffee
{
public string GetDescription() => "Coffee";
public decimal GetCost() => 2.00m;
}
public abstract class CoffeeDecorator : ICoffee
{
protected ICoffee _coffee;
public CoffeeDecorator(ICoffee coffee) => _coffee = coffee;
public virtual string GetDescription() => _coffee.GetDescription();
public virtual decimal GetCost() => _coffee.GetCost();
}
public class MilkDecorator : CoffeeDecorator
{
public MilkDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", Milk";
public override decimal GetCost() => _coffee.GetCost() + 0.50m;
}Decorators can be stacked by wrapping a decorated object with another decorator:
ICoffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new MilkDecorator(coffee); // Double milk!
Console.WriteLine(coffee.GetDescription()); // Coffee, Milk, Milk
Console.WriteLine(coffee.GetCost()); // 3.00Use the Decorator pattern when you need to add responsibilities to objects without creating many subclasses. Each decorator focuses on a single concern and can be combined as needed.
Try it yourself
using System;
using PizzaShop;
class Program
{
public static void Main(string[] args)
{
// Read the comma-separated list of toppings
string input = Console.ReadLine();
string[] toppings = input.Split(',');
// TODO: Start with a PlainPizza
// TODO: Loop through each topping and wrap the pizza
// with the appropriate decorator based on the topping name:
// - "cheese" -> CheeseTopping
// - "pepperoni" -> PepperoniTopping
// - "mushrooms" -> MushroomTopping
// TODO: Print the final description and price
}
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of OOP
External FilesNamespaces & DirectivesIntro to Classes & ObjectsThe 'this' KeywordMethods and ParametersFields vs PropertiesConstructorsObject InitializersRecap - Simple Calculator4Inheritance
Basic Inheritance (:) SyntaxThe 'base' KeywordVirtual & Override KeywordsSealed ClassesThe 'object' Base ClassRecap - Employee Hierarchy7Advanced Features
Operator OverloadingIndexers (this[])ToString() OverrideExtension MethodsRecap - Custom List2Properties & Static Members
Auto-Implemented PropertiesRead/Write-Only PropertiesStatic Fields & MethodsStatic ClassesExpression-Bodied Members5Polymorphism & Interfaces
Compile vs Runtime PolyInterface vs Abstract ClassMultiple InterfacesExplicit InterfacesUpcasting & DowncastingRecap - Shape Calculator8Advanced OOP Concepts
Composition over InheritanceGenerics (Classes & Methods)Delegates and EventsAttributes and ReflectionIDisposable & using StatementDependency Injection Basics11Design Patterns Part 2
Command PatternAdapter PatternDecorator PatternTemplate Method PatternState PatternComposite Pattern