• 设计模式-装饰器模式


    示例

    对于装饰器模式,我想先不谈概念,而是先从一个例子开始说起,看看面对这样的需求,我们应该如何处理,并希望由此逐步引出装饰器模式以加深理解。

    需求

    假设现在需要开一个奶茶店,奶茶种类繁多,如红豆奶茶,布丁奶茶,珍珠奶茶,红豆珍珠奶茶等。种类虽多,但实质上都是在奶茶中加了各种配料而已。为了简化实现,继续假设奶茶的价格根据奶茶本身加上不同配料累计计算而成。然后,根据每个客户的要求,每种奶茶又可以加糖或者加冰,加糖加冰不额外收费。

    初级方案

    在学习设计模式之前,或许最容易想到的方案就是继承了,即先定义奶茶类,然后再定义各种奶茶子类继承自奶茶类,考虑到以后或许还会有更多的饮品,例如咖啡,因此再定义一个饮品的抽象基类,让奶茶类继承自饮品基类,这样一来,最终设计可能会如下类图所示:

    部分代码如下:

    public abstract class Drink
    {
        public string Name { get; set; }
    
        public int Price { get; set; }
    
        public abstract string Desc { get; }
    
        public abstract int Cost { get; }
    }
    
    public class Naicha : Drink
    {
        public Naicha()
        {
            Name = "奶茶";
            Price = 8;
        }
        public override string Desc => this.Name;
        public override int Cost => this.Price;
    }
    
    public class HongDouNaicha : Naicha
    {
        public HongDouNaicha()
        {
            Name += "+红豆";
            Price += 1;
        }
    }
    
    public class ZhenzhuNaicha : Naicha
    {
        public ZhenzhuNaicha()
        {
            Name += "+珍珠";
            Price += 3;
        }
    }
    
    ...
    

    问题

    不难想象,这种设计是一种灾难,因为它至少会出现如下四个问题:

    • 类爆炸,代码虽然只列了部分,但通过类图可以看出类的数量必定会达到一个恐怖的地步;
    • 如果加冰改为收费,需要多处修改价格,代码维护困难,严重违反开闭原则;
    • 如果新增配料,类的数量会急剧增加,代码维护困难,严重违反开闭原则;
    • 无法实现加多份配料,如多冰、多糖等。

    改进一

    由于上述问题,现实促使我们不得不对方案进行改进,不过好在对于类爆炸的问题,我们是有经验的,我们在学习工厂方法模式的时候就出现过类爆炸,我们通过合并的方式就演化出了抽象工厂模式,这里我们也可以依葫芦画瓢,对类进行合并。
    合并后的类图如下:

    再看看代码:

    public abstract class Drink
    {
        public string Name { get; set; }
    
        public int Price { get; set; }
    
        public abstract string Desc { get; }
    
        public abstract int Cost { get; }
    
        public abstract void AddBuding();
    
        public abstract void AddHongdou();
    
        public abstract void AddZhenzhu();
    
        public abstract void AddBing();
    
        public abstract void AddTang();
    }
    
    public class Naicha : Drink
    {
        private string _desc = string.Empty;
        private int _cost = 0;
        public Naicha()
        {
            Name = "奶茶";
            Price = 8;
        }
    
        public override string Desc => this.Name + _desc;
        public override int Cost => this.Price + _cost;
    
        public override void AddBing()
        {
            _desc += "+冰";
        }
    
        public override void AddBuding()
        {
            _desc += "+布丁";
            _cost += 2;
        }
    
        public override void AddHongdou()
        {
            _desc += "+红豆";
            _cost += 1;
        }
    
        public override void AddTang()
        {
            _desc += "+糖";
        }
    
        public override void AddZhenzhu()
        {
            _desc += "+珍珠";
            _cost += 3;
        }
    }
    

    优点

    将各种子类都直接改成抽象方法放到Drink父类中,效果简直立杆见影,起码解决了初级方案中的两个问题:

    • 消除了类爆炸的问题,代码简洁,一下子就只剩下两个类了;
    • 配料可以任意搭配组合,并且也可以加入多份。

    缺点

    但是新的问题也随之而来:

    • 如果修改价格或新增配料就需要新增方法,违反了开闭原则;
    • 如果新增饮品咖啡,这时也会变得麻烦,因为咖啡需要冰和糖,同时还需要咖啡伴侣,但是不需要布丁、珍珠、红豆等。
      public abstract class Drink
      {
          ...
      
          public abstract void AddKafeibanlv();
      }
      
      public class Naicha : Drink
      {
          ...
      
          public override void AddKafeibanlv()
          {
      
          }
      }
      
      public class Kafei : Drink
      {
          ...
      
          public override void AddBuding()
          {
      
          }
      
          public override void AddHongdou()
          {
      
          }
      
          public override void AddZhenzhu()
          {
      
          }
      
          public override void AddKafeibanlv()
          {
              _desc += "+咖啡伴侣";
              _cost += 2;
          }
      }
      
      可以看到,增加了咖啡之后,父类以及每个子类的代码都要跟着修改,而且每个子类都必须继承大量无用的方法。

    改进二

    因此,我们还需要进一步改进,这次我们改进的方向是将这些方法抽象并合并,因为我们可以看到,上面的方案之所以会有这么多问题就是因为面向了实现编程,每个方法都代表了一种配料,如果我们将这些配料全部继承自同一个抽象类,然后提供一个面向抽象的AddPeiliao(Peiliao peiliao)方法不就可以这个问题了吗?于是我们就有了如下改进:

    为了满足这个需求,我们对饮品基类也进行了较大的改造,代码如下:

    public abstract class Drink
    {
        protected List<Peiliao> Peiliaos = new List<Peiliao>();
        public string Name { get; set; }
    
        public int Price { get; set; }
    
        public int Cost
        {
            get
            {
                int cost = this.Price;
                foreach (var peiliao in Peiliaos)
                {
                    cost += peiliao.Price;
                }
                return cost;
            }
        }
    
        public string Desc
        {
            get
            {
                string desc = this.Name;
                foreach (var peiliao in Peiliaos)
                {
                    desc += "+" + peiliao.Name;
                }
                return desc;
            }
        }
    
        public void AddPeiliao(Peiliao peiliao)
        {
            Peiliaos.Add(peiliao);
        }
    }
    
    public class Naicha : Drink
    {
        public Naicha()
        {
            Name = "奶茶";
            Price = 8;
        }
    }
    

    由于配料全部通过一个集合组合到了基类中,因此,不需要通过抽象方法让子类计算价格,而是直接在基类中循环叠加计算,同时,由于大部分的功能都在基类中实现了,子类变得干净简洁了。

    再看看配料:

    public abstract class Peiliao
    {
        public abstract string Name { get; }
    
        public abstract int Price { get; }
    }
    
    public class Buding : Peiliao
    {
        public override string Name => "布丁";
    
        public override int Price => 2;
    }
    

    同样的简洁干净。

    优点

    这样的改进优点是巨大的,几乎解决了所有问题:

    • 配料可任意搭配组合,并且满足新增饮品的需求;
    • 新增饮品和配料均只需要增加新的类即可,满足开闭原则

    缺点

    感觉上好像挺不错的,堪称完美!难道这就是今天的主角---装饰器模式?其实,我们忽略了两个问题:

    • 这个方案以及上一个方案都犯了一个致命的错误,就是修改了饮品类,这在很多时候是不被允许的,或者说根本做不到的,就好比我们要给手机加个装饰---贴个膜,难道我们要先改一下手机的内部结构吗?这明显是不合理,也是做不到的。
    • Add方法也不太合理,饮料不应该具有添加配料的能力,这好比给了手机一个膜,手机自己贴上了,总觉得哪里怪怪的。

    改进三

    其实,从设计原则的角度来讲,上一个方案的改进已经很接近了,因为它已经满足了开闭原则,扩展性方面也非常优秀,唯一的问题就是需要修改奶茶类,这通常是不能实现的。那么,我们思路再次转变一下,奶茶类不能改,但是配料可以改啊,我们换个依赖方向,将奶茶聚合到配料中不就可以了吗?于是就有了如下类图:

    再看看代码:

    public abstract class Drink
    {
        public string Name { get; set; }
    
        public int Price { get; set; }
    
        public abstract string Desc { get; }
    
        public abstract int Cost { get; }
    }
    

    饮品类还原到了最初状态,没做任何修改。

    public class Peiliao:Drink
    {
        protected readonly Drink Drink;
    
        public Peiliao(Drink drink)
        {
            Drink = drink;
        }
    
        public override string Desc
        {
            get
            {
                return Drink.Desc + "+" + this.Name;
            }
        }
    
        public override int Cost
        {
            get
            {
                return Drink.Cost + this.Price;
            }
        }
    }
    

    将饮品类聚合到了配料类中,但是这里和前一个方案又有所不同,因为配料毕竟是配料,聚合方向换了之后,通过new就只能得到配料而得不到奶茶了,因此,为了最终能得到奶茶,我们的配料也必须继承自饮品类,这看起来很怪,但妙也妙在这里,通过聚合+继承的方式改进,可使得饮品的扩展更灵活,同时也遵守了开闭原则。其中,聚合是为了实现功能,而继承是为了约束类型,这就是装饰者模式。

    定义

    装饰器模式动态地给一个对象增加一些额外的职责。就增加功能而言,装饰器模式比生成子类更为灵活。

    UML类图

    优缺点

    优点

    • 可动态的给一个对象增加额外的职责
    • 有很好地可扩展性

    缺点

    • 增加了程序的复杂度,刚接触理解起来会比较困难

    跟代理模式的区别

    装饰器模式跟代理模式类图十分相似,但是,它们之间却有很大的区别:

    • 装饰器模式关注于在一个对象上动态的添加方法,而代理模式关注于控制对对象的访问。
    • 装饰器模式通常用聚合的方式,而代理模式通常采用组合的方式。
    • 装饰器模式通常会套用多层,而代理模式通常只有一层。

    但是由于他们的结构十分相似,因此很多时候二者可以做同样的事,比如装饰器模式和代理模式都可用于实现AOP(面向切面编程)。

    经典案例

    在.NET类库中,System.IO.Stream就是装饰者模式的一个经典案例,不过在这个案例中没有用到Decorator基类。

    总结

    装饰器模式可以说是结构型设计模式的巅峰之作,其中设计思想十分精妙,但理解起来也确实有些困难,因此,可能还是需自己动手撸码,加深体会。

    源码链接

  • 相关阅读:
    第七章-方法区
    wchar_t 字符拼接
    C++获取appdata路径
    char * 、BSTR、long、wchar_t *、LPCWSTR、string、QString类型转换
    climits 与 符号常量
    Qt数据结构-QString二:QString的arg能不能像Python的format一样使用
    Qt数据结构-QString一:常用方法
    怎么查看摄像头的硬件ID
    jenkins提示使用java11版本
    Jenkins:the input device is not a TTY
  • 原文地址:https://www.cnblogs.com/FindTheWay/p/13605360.html
Copyright © 2020-2023  润新知