• 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏吧

设计模式-装饰器模式

开发技术 开发技术 4周前 (09-02) 26次浏览

示例

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

需求

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

初级方案

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

部分代码如下:

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基类。
设计模式-装饰器模式

总结

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

源码链接


喜欢 (0)