装饰器模式(Decorator Pattern)

1. 模式概述

装饰器模式(Decorator Pattern) 是一种结构型设计模式,允许动态地向一个对象添加额外的职责,而不需要修改其结构。它通过创建一系列装饰器类来包装原始对象,提供了比继承更灵活的扩展方式。

2. 模式结构

主要角色:

  1. 组件接口(Component):定义对象的通用接口

  2. 具体组件(ConcreteComponent):实现组件接口的基本对象

  3. 装饰器基类(Decorator):维护对组件对象的引用,并实现组件接口

  4. 具体装饰器(ConcreteDecorator):向组件添加具体的额外职责

3. 示例 - 奶茶订单系统

以下订单系统是对装饰器模式的简单应用。

  • 组件接口
C# 复制代码
// 组件接口 - 奶茶接口
public interface IMilkTea
{
    // 价格
    double Cost { get; set; }
    // 描述
    string Description { get; }
    // 获取价格
    double GetCost();
    // 获取规格
    string GetSize();
}
  • 具体组件

具体组件是装饰器需要修饰的主体。

这里我们定义了几种常见奶茶:经典奶茶、乌龙奶茶、茉莉奶茶、红糖奶茶

C# 复制代码
// 具体组件 - 经典奶茶
public class ClassicMilkTea : IMilkTea
{
    // 规格
    private string? _size;
    // 价格
    public double Cost { get; set; }
    // 构造函数,默认中杯
    public ClassicMilkTea(string? size = "中杯")
    {
        _size = size;
        CalculateCost();
    }
    // 描述
    public string Description => $"经典奶茶({_size})";
    // 获取价格
    public double GetCost() => Cost;
    // 获取规格
    public string GetSize() => _size ?? "中杯";

    public void CalculateCost()
    {
        double basePrice = 6.0; // 基础价格为中杯价格
        Cost = _size switch
        {
            "小杯" => basePrice * 0.8,
            "中杯" => basePrice,
            "大杯" => basePrice * 1.2,
            _ => basePrice,
        };
    }
}

// 具体组件 - 乌龙奶茶
public class OolongMilkTea : IMilkTea
{
    // 规格
    private string? _size;
    // 价格
    public double Cost { get; set; }
    
    public OolongMilkTea(string size = "中杯")
    {
        _size = size;
        CalculateCost();
    }

    
    public string Description => $"乌龙奶茶({_size})";

    public double GetCost() => Cost;

    public string GetSize() => _size ?? "中杯";

    private void CalculateCost()
    {
        double basePrice = 8.0; // 基础价格为中杯价格
        Cost = _size switch
        {
            "小杯" => basePrice * 0.8,
            "中杯" => basePrice,
            "大杯" => basePrice * 1.2,
            _ => basePrice,
        };
    }
}

// 具体组件 - 茉莉奶茶
public class JasmineMilkTea : IMilkTea
{
    private string? _size;

    public double Cost { get; set; }

    public JasmineMilkTea(string size = "中杯")
    {
        _size = size;
        CalculateCost();
    }
    public string Description => $"茉莉奶茶({_size})";
    public double GetCost() => Cost;
    public string GetSize() => _size ?? "中杯";
    public void CalculateCost()
    {
        double basePrice = 7.0; // 基础价格为中杯价格
        Cost = _size switch
        {
            "小杯" => basePrice * 0.8,
            "中杯" => basePrice,
            "大杯" => basePrice * 1.2,
            _ => basePrice,
        };
    }
}

// 具体组件 - 红糖奶茶
public class BrownSugarMilkTea : IMilkTea
{
    private string? _size;
    public double Cost { get; set; }
    public BrownSugarMilkTea(string size = "中杯")
    {
        _size = size;
        CalculateCost();
    }
    public string Description => $"红糖奶茶({_size})";
    public double GetCost() => Cost;
    public string GetSize() => _size ?? "中杯";
    public void CalculateCost()
    {
        double basePrice = 9.0; // 基础价格为中杯价格
        Cost = _size switch
        {
            "小杯" => basePrice * 0.8,
            "中杯" => basePrice,
            "大杯" => basePrice * 1.2,
            _ => basePrice,
        };
    }
}
  • 装饰器基类

这是一个抽象类,装饰器可以继承他并实现具体的装饰布置。

C# 复制代码
// 装饰器抽象类 - 配料装饰器
public abstract class ToppingDecorator : IMilkTea
{
    // 被装饰的奶茶对象
    protected IMilkTea _milkTea;
    // 价格
    public double Cost { get; set; }

    protected ToppingDecorator(IMilkTea milkTea)
    {
        _milkTea = milkTea;
    }
    // 描述
    public abstract string Description { get; }
    // 获取价格
    public abstract double GetCost();
    // 获取规格
    public virtual string GetSize() => _milkTea.GetSize();
}
  • 具体装饰器

具体装饰器一般通过继承装饰器基类并进行功能的具现,也可以对功能进行扩展。

这里罗列了珍珠、椰果、布丁等常见奶茶配料以及加冰、加热的常见选项。

C# 复制代码
// 具体装饰器 - 珍珠配料
public class PearlDecorator : ToppingDecorator
{
    public PearlDecorator(IMilkTea milkTea) : base(milkTea)
    {
        Cost = 2.0; // 珍珠配料价格
    }

    public override string Description => _milkTea.Description + " + 珍珠";

    public override double GetCost() => _milkTea.GetCost() + Cost;
}

// 具体装饰器 - 椰果配料
public class CoconutJellyDecorator : ToppingDecorator
{
    public CoconutJellyDecorator(IMilkTea milkTea) : base(milkTea)
    {
        Cost = 1.5; // 椰果配料价格
    }

    public override string Description => _milkTea.Description + " + 椰果";

    public override double GetCost() => _milkTea.GetCost() + Cost;
}

// 具体装饰器 - 布丁配料
public class PuddingDecorator : ToppingDecorator
{
    public PuddingDecorator(IMilkTea milkTea) : base(milkTea)
    {
        Cost = 2.5; // 布丁配料价格
    }

    public override string Description => _milkTea.Description + " + 布丁";

    public override double GetCost() => _milkTea.GetCost() + Cost;
}

// 具体装饰器 - 红豆配料
public class RedBeanDecorator : ToppingDecorator
{
    public RedBeanDecorator(IMilkTea milkTea) : base(milkTea)
    {
        Cost = 2.0; // 红豆配料价格
    }
    public override string Description => _milkTea.Description + " + 红豆";
    public override double GetCost() => _milkTea.GetCost() + Cost;
}

// 具体装饰器 - 仙草配料
public class GrassJellyDecorator : ToppingDecorator
{
    public GrassJellyDecorator(IMilkTea milkTea) : base(milkTea)
    {
        Cost = 1.8; // 仙草配料价格
    }
    public override string Description => _milkTea.Description + " + 仙草";
    public override double GetCost() => _milkTea.GetCost() + Cost;
}

// 具体装饰器 - 芝士奶盖配料
public class CheeseCreamDecorator : ToppingDecorator
{
    public CheeseCreamDecorator(IMilkTea milkTea) : base(milkTea)
    {
        Cost = 3.0; // 芝士奶盖配料价格
    }
    public override string Description => _milkTea.Description + " + 芝士奶盖";
    public override double GetCost() => _milkTea.GetCost() + Cost;
}

// 具体装饰器 - 冰块等级装饰器
public class IceLevelDecorator : ToppingDecorator
{
    private string _iceLevel;

    public IceLevelDecorator(IMilkTea milkTea, string iceLevel) : base(milkTea)
    {
        _iceLevel = iceLevel;
        Cost = 0.0; // 冰块等级不额外收费
    }

    public override string Description => _milkTea.Description + $"({_iceLevel}冰)";

    public override double GetCost() => _milkTea.GetCost() + Cost;
}

// 具体装饰器 - 糖度等级装饰器
public class SugarLevelDecorator : ToppingDecorator
{
    private string _sugarLevel;
    public SugarLevelDecorator(IMilkTea milkTea, string sugarLevel) : base(milkTea)
    {
        _sugarLevel = sugarLevel;
        Cost = 0.0; // 糖度等级不额外收费
    }
    public override string Description => _milkTea.Description + $"({_sugarLevel}糖)";
    public override double GetCost() => _milkTea.GetCost() + Cost;
}

// 具体装饰器 - 热饮装饰器
public class HotDecorator : ToppingDecorator
{
    public HotDecorator(IMilkTea milkTea) : base(milkTea)
    {
        Cost = 0.0; // 热饮不额外收费
    }
    public override string Description => "[热饮]" + _milkTea.Description;
    public override double GetCost() => _milkTea.GetCost() + Cost;
}

// 具体装饰器 - 去冰装饰器
public class NoIceDecorator : ToppingDecorator
{
    public NoIceDecorator(IMilkTea milkTea) : base(milkTea)
    {
        Cost = 0.0; // 无冰不额外收费
    }
    public override string Description => _milkTea.Description + "(去冰)";
    public override double GetCost() => _milkTea.GetCost() + Cost;
}

当然,外带服务、折扣也可以使用装饰器进行订单的修改。

C# 复制代码
// 具体装饰器 - 外带装饰器
public class TakeawayDecorator : ToppingDecorator
{
    public TakeawayDecorator(IMilkTea milkTea) : base(milkTea)
    {
        Cost = 1.0; // 外带加收1元
    }
    public override string Description => _milkTea.Description + "(外带)";
    public override double GetCost() => _milkTea.GetCost() + Cost;
}

// 具体装饰器 - 折扣装饰器
public class DiscountDecorator : ToppingDecorator
{
    private double _discountRate;
    public DiscountDecorator(IMilkTea milkTea, double discountRate) : base(milkTea)
    {
        _discountRate = discountRate;
        Cost = 0.0; // 折扣不额外收费
    }
    public override string Description => _milkTea.Description + $"(折扣{_discountRate * 100}%)";
    public override double GetCost() => _milkTea.GetCost() * _discountRate;
}

我们甚至可以通过扩展装饰器方法达到调整奶茶并为其增加额外费用的目的。

C# 复制代码
/// <summary>
/// 为奶茶配料提供装饰功能,该功能可追踪基础奶茶并为其增加额外费用。
/// </summary>
/// <remarks>使用此装饰器可为奶茶订单中的配料添加特定费用。该装饰器会更新描述以包含费用,并可调整支持的奶茶类型的基础费用。此类通常用于需要单独列出各配料费用的场景。</remarks>
public class CostTrackingDecorator : ToppingDecorator
{
    // 额外费用
    private double _additionalCost;
    // 构造函数,接受基础奶茶对象和额外费用
    public CostTrackingDecorator(IMilkTea milkTea, double additionalCost, string toppingName) : base(milkTea)
    {
        _additionalCost = additionalCost;
        Cost = additionalCost;
    }

    public override string Description => _milkTea.Description + $"[{Cost}]";

    public override double GetCost() => _milkTea.GetCost() + Cost;
    /// <summary>
    /// 通过将当前成本加到总成本中,来更新基础奶茶的成本。
    /// </summary>
    /// <remarks>此方法仅在基础奶茶类型为ClassicMilkTea(经典奶茶)、OolongMilkTea(乌龙奶茶)或BrownSugarMilkTea(红糖奶茶)时应用成本调整。若奶茶类型不同,则不进行任何更改。</remarks>
    public void UpdateBaseCost()
    {
        if(_milkTea is ClassicMilkTea classic)
        {
            classic.Cost += Cost;
        }
        else if(_milkTea is OolongMilkTea oolong)
        {
            oolong.Cost += Cost;
        }else if(_milkTea is BrownSugarMilkTea brownsugar)
        {
            brownsugar.Cost += Cost;
        }
    }
}

最后通过对装饰器的具体应用便可以达到订单系统的简单使用。

  • 工厂类
C# 复制代码
// 奶茶工厂类
public class MilkTeaFactory
{
    public static IMilkTea CreateMilkTea(string type, string size = "中杯")
    {
        return type switch
        {
            "经典" or "Classic" => new ClassicMilkTea(size),
            "乌龙" or "Oolong" => new OolongMilkTea(size),
            "茉莉" or "Jasmine" => new JasmineMilkTea(size),
            "红糖" or "BrownSugar" => new BrownSugarMilkTea(size),
            _ => new ClassicMilkTea(size),
        };
    }
}
  • 奶茶订单类
C# 复制代码
// 奶茶订单类
public class MilkTeaOrder
{
    private List<IMilkTea> _item = [];

    public void AddItem(IMilkTea milkTea)
    {
        _item.Add(milkTea);
    }

    public void RemoveItem(IMilkTea milkTea)
    {
        _item.Remove(milkTea);
    }

    public void PrintOrder()
    {
        Console.WriteLine("========== 奶茶订单 ==========");
        Console.WriteLine("序号\t商品\t\t\t价格");
        Console.WriteLine(new string('-', 50));
        double totalCost = 0.0;
        for (int i=0; i < _item.Count; i++)
        {
            var item = _item[i];
            Console.WriteLine($"{i + 1}\t{item.Description}\t¥{item.GetCost():F2}");
            totalCost += item.GetCost();
        }
        Console.WriteLine(new string('-', 50));
        Console.WriteLine($"{_item.Count}件商品\t\t总价格: ¥{totalCost:F2}");
        Console.WriteLine("==============================\n");
    }
    public double GetTotalCost()
    {
        double totalCost = 0.0;
        foreach (var milkTea in _item)
        {
            totalCost += milkTea.GetCost();
        }
        return totalCost;
    }
    public int GetItemCount()
    {
        return _item.Count;
    }
    public void PrintCostDetails()
    {
        Console.WriteLine("奶茶订单价格详情:");
        foreach (var milkTea in _item)
        {
            Console.WriteLine($"{milkTea.Description}: ¥{milkTea.GetCost():F2}");
        }
        Console.WriteLine($"总价格: ¥{GetTotalCost():F2}\n");
    }
}
  • 订单系统实现
C# 复制代码
Console.WriteLine("Hello, World!");
Console.WriteLine("=== 奶茶店订单系统 (使用Cost属性和GetCost方法) ===\n");

// 创建订单
MilkTeaOrder order = new MilkTeaOrder();

// 1.添加经典奶茶
Console.WriteLine("=== 基础奶茶示例 ===");
IMilkTea classic = new ClassicMilkTea("大杯");
Console.WriteLine($"{classic.Description}");
Console.WriteLine($"Cost属性:¥{classic.Cost:F2}");
Console.WriteLine($"GetCost():¥{classic.GetCost():F2}");
Console.WriteLine();

// 2.装饰后的奶茶示例
Console.WriteLine("=== 装饰后的奶茶示例 ===");
IMilkTea oolongWithToppings = new OolongMilkTea("中杯");
// 添加珍珠
oolongWithToppings = new PearlDecorator(oolongWithToppings);
// 添加椰果
oolongWithToppings = new CoconutJellyDecorator(oolongWithToppings);
Console.WriteLine($"{oolongWithToppings.Description}");
// 使用Cost属性和GetCost()方法获取价格的区别
// 这里获取的Cost属性是最后一个装饰器的Cost属性值(即椰果的价格)
Console.WriteLine($"Cost属性:¥{oolongWithToppings.Cost:F2}");
// 这里获取的GetCost()方法返回的是包含配料费用的总价格
Console.WriteLine($"GetCost():¥{oolongWithToppings.GetCost():F2}");
Console.WriteLine();

// 3.完整订单
Console.WriteLine("=== 完整订单 ===");

// 订单1: 经典奶茶大杯 + 珍珠 + 椰果
IMilkTea order1 = MilkTeaFactory.CreateMilkTea("经典","大杯");
Console.WriteLine($"基础价格: ¥{order1.GetCost():F2}");
order1 = new PearlDecorator(order1);
Console.WriteLine($"加珍珠后:¥{order1.GetCost():F2}");
order1 = new CoconutJellyDecorator(order1);
Console.WriteLine($"加椰果后:¥{order1.GetCost():F2}");
order1 = new IceLevelDecorator(order1,"正常");
order1 = new SugarLevelDecorator(order1,"正常");
Console.WriteLine($"订单详情:{order1.Description}:¥{order1.GetCost():F2}");
order.AddItem(order1);

// 订单2: 黑糖奶茶中杯 + 布丁 + 奶盖 + 外带
IMilkTea order2 = MilkTeaFactory.CreateMilkTea("黑糖","中杯");
Console.WriteLine($"\n基础价格: ¥{order2.GetCost():F2}");
order2 = new PuddingDecorator(order2);
Console.WriteLine($"加布丁后:¥{order2.GetCost():F2}");
order2 = new CheeseCreamDecorator(order2);
Console.WriteLine($"加奶盖后:¥{order2.GetCost():F2}");
order2 = new TakeawayDecorator(order2);
Console.WriteLine($"加外带后:¥{order2.GetCost():F2}");
order2 = new IceLevelDecorator(order2,"正常");
Console.WriteLine($"订单详情:{order2.Description}:¥{order2.GetCost():F2}");
order.AddItem(order2);

// 订单3: 乌龙奶茶小杯 + 折扣
IMilkTea order3 = MilkTeaFactory.CreateMilkTea("乌龙","小杯");
Console.WriteLine($"\n基础价格: ¥{order3.GetCost():F2}");
order3 = new DiscountDecorator(order3,0.8); // 打折20%
Console.WriteLine($"折扣后:¥{order3.GetCost():F2}");
Console.WriteLine($"订单详情:{order3.Description}:¥{order3.GetCost():F2}");
order.AddItem(order3);

// 打印完整订单
Console.WriteLine("\n=== 订单汇总 ===");
order.PrintOrder();

运行代码,得到如下效果:

bash 复制代码
Hello, World!
=== 奶茶店订单系统 (使用Cost属性和GetCost方法) ===

=== 基础奶茶示例 ===
经典奶茶(大杯)
Cost属性:¥7.20
GetCost():¥7.20

=== 装饰后的奶茶示例 ===
乌龙奶茶(中杯) + 珍珠 + 椰果
Cost属性:¥1.50
GetCost():¥11.50

=== 完整订单 ===
基础价格: ¥7.20
加珍珠后:¥9.20
加椰果后:¥10.70
订单详情:经典奶茶(大杯) + 珍珠 + 椰果(正常冰)(正常糖):¥10.70

基础价格: ¥6.00
加布丁后:¥8.50
加奶盖后:¥11.50
加外带后:¥12.50
订单详情:经典奶茶(中杯) + 布丁 + 芝士奶盖(外带)(正常冰):¥12.50

基础价格: ¥6.40
折扣后:¥5.12
订单详情:乌龙奶茶(小杯)(折扣80%):¥5.12

=== 订单汇总 ===
========== 奶茶订单 ==========
序号    商品                    价格
--------------------------------------------------
1       经典奶茶(大杯) + 珍珠 + 椰果(正常冰)(正常糖)    ¥10.70
2       经典奶茶(中杯) + 布丁 + 芝士奶盖(外带)(正常冰)  ¥12.50
3       乌龙奶茶(小杯)(折扣80%) ¥5.12
--------------------------------------------------
3件商品         总价格: ¥28.32
==============================

4. 装饰器模式的优势

  • 优点:

    • 比继承更灵活:装饰器模式允许动态地添加或删除功能,而继承是静态的

    • 避免在层次结构高层的类有太多的特征:可以按需组合装饰器

    • 符合开闭原则:对扩展开放,对修改关闭

    • 职责分离:每个装饰器类只关注一个特定的功能

  • 缺点:

    • 会产生许多小对象:过度使用会增加系统复杂性

    • 装饰器顺序很重要:不同顺序可能产生不同结果

    • 多层装饰时调试困难:调用链可能很长

5. 适用场景

  • 在不影响其他对象的情况下,动态、透明地给单个对象添加职责

  • 需要为对象添加的功能可以动态地撤销

  • 当不能使用继承扩展功能时(如 sealed 类)

  • 常见应用场景:

    • GUI 组件的边框、滚动条、颜色等装饰

    • 流处理(压缩、加密、缓冲等)

    • 文本格式化(加粗、斜体、颜色等)

    • 权限验证、日志记录、性能监控等横切关注点

6. 实际应用建议

  • 最佳实践:

    • 保持装饰器接口与组件接口一致:确保透明性

    • 简化装饰器类:每个装饰器应该只负责一个小的功能

    • 考虑装饰器的顺序:某些装饰器需要特定的顺序

    • 提供工厂方法:简化复杂装饰器的创建

  • 透明式 vs 半透明式:

C# 复制代码
// 透明式装饰器(推荐)
public abstract class Decorator : IComponent
{
    // 实现所有接口方法
}

// 半透明式装饰器(谨慎使用)
public abstract class SpecialDecorator : IComponent
{
    // 新增额外方法
    public void ExtraMethod() { }
}

#7.与其他模式的关系

  • 与适配器模式:适配器改变对象接口,装饰器增强对象功能

  • 与代理模式:代理控制访问,装饰器添加功能

  • 与组合模式:装饰器可被视为只有一个组件的组合

  • 与策略模式:装饰器改变对象外表,策略改变对象内核

8..NET 中的装饰器模式应用

实际案例:

C# 复制代码
// .NET Stream 装饰器示例
using (var fileStream = new FileStream("data.txt", FileMode.Open))
using (var bufferedStream = new BufferedStream(fileStream))  // 缓冲装饰器
using (var cryptoStream = new CryptoStream(bufferedStream, encryptor, CryptoStreamMode.Write))  // 加密装饰器
using (var gzipStream = new GZipStream(cryptoStream, CompressionMode.Compress))  // 压缩装饰器
{
    // 写入数据
}

9.总结

装饰器模式提供了一种强大而灵活的方式来扩展对象功能,而不需要修改原始类。通过组合不同的装饰器,可以创建出功能丰富的对象,同时保持代码的模块化和可维护性。在需要动态、可插拔地添加功能的场景中,装饰器模式是一个非常有效的解决方案。

相关推荐
杨充1 小时前
10.可测试性实战设计
设计模式·开源·代码规范
杨充1 小时前
9.重构十二式的实战
设计模式·开源·代码规范
杨充1 小时前
6.设计原则的全景图
设计模式·开源·全栈
杨充1 小时前
2.面向对象的特性
设计模式
杨充1 小时前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充1 小时前
8.反模式与坏味道
设计模式·开源·代码规范
杨充2 小时前
3.接口vs抽象类比较
设计模式
咖啡八杯5 小时前
文法、BNF与AST
java·设计模式·解释器模式·ast·文法
咖啡八杯13 小时前
GoF设计模式——解释器模式
java·后端·spring·设计模式