Menu
CoddyTech

装饰器模式

本课属于 Coddy C# 学习旅程的“面向对象编程”单元。第 58 课,共 70 课。

装饰器模式是一种结构型模式,可以通过用称为装饰器的特殊对象包装对象,为对象添加新的行为。与在编译时添加行为的继承不同,装饰器可以在运行时动态添加功能,而无需修改原始类。

这种模式通过让装饰器实现与被包装对象相同的接口来工作。每个装饰器都持有对被包装对象的引用,并将调用委托给它,在调用之前或之后添加自身的行为:

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;
}

装饰器的强大之处在于它们可以叠加。你可以用另一个装饰器包装已经装饰过的对象:

ICoffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new MilkDecorator(coffee);  // 双份牛奶!

Console.WriteLine(coffee.GetDescription());  // Coffee, Milk, Milk
Console.WriteLine(coffee.GetCost());         // 3.00

当你需要为对象添加职责,又不想创建大量子类时,装饰器模式就很合适。每个装饰器只关注一个方面,你可以按需组合它们,构建出想要的准确行为。

challenge icon

挑战

简单

让我们使用装饰器模式构建一个披萨订购系统。你将创建一个基础披萨,然后动态地添加配料。每种配料都会包装上一层,并增加披萨的描述和价格。

你将把代码分放在 3 个文件中:

  • Pizza.cs:在 PizzaShop 命名空间中定义 IPizza 接口,其中包含两个方法:GetDescription() 返回字符串,GetPrice() 返回 decimal。然后创建一个实现此接口的 PlainPizza 类,它的描述应返回 "Pizza",基础价格应为 8.00m。
  • ToppingDecorators.cs:创建一个实现 IPizza 的抽象类 ToppingDecorator,并保存对被包装披萨的引用(通过构造函数传入)。它的虚方法应委托给被包装的披萨。然后创建 3 个具体装饰器:
    • CheeseTopping:在描述后追加 ", Cheese",并在价格上增加 1.50m
    • PepperoniTopping:在描述后追加 ", Pepperoni",并在价格上增加 2.00m
    • MushroomTopping:在描述后追加 ", Mushrooms",并在价格上增加 1.25m
  • Program.cs:从一个普通披萨开始,根据输入用配料包装它,将所有内容组合起来。每个配料装饰器都会包装上一层披萨,逐层构建最终订单。

你将收到一项输入:

  • 以逗号分隔的配料列表(例如:cheese,pepperoni,cheese)

从 PlainPizza 开始,然后按照配料出现的顺序,用相应的装饰器包装它。有效配料包括 cheese、pepperoni 和 mushrooms。添加完所有配料后,将描述打印在一行,下一行打印总价。

例如,如果输入是 cheese,pepperoni,mushrooms,输出应为:

Pizza, Cheese, Pepperoni, Mushrooms
12.75

注意,你可以多次添加相同的配料!如果输入是 cheese,cheese,结果将是:

Pizza, Cheese, Cheese
11.00

这展示了装饰器模式的灵活性:每个装饰器都会包装传入的披萨,无论它是普通披萨还是已经装饰过的披萨,因此你可以动态地叠加行为!

亲手试试

using System;
using PizzaShop;

class Program
{
    public static void Main(string[] args)
    {
        // 读取用逗号分隔的配料列表
        string input = Console.ReadLine();
        string[] toppings = input.Split(',');
        
        // TODO: 从 PlainPizza 开始
        
        // TODO: 遍历每种配料,并根据配料名称
        // 用对应的装饰器包装披萨:
        // - "cheese" -> CheeseTopping
        // - "pepperoni" -> PepperoniTopping
        // - "mushrooms" -> MushroomTopping
        
        // TODO: 输出最终的描述和价格
    }
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: C# 在线编译器