示例
对于装饰器模式,我想先不谈概念,而是先从一个例子开始说起,看看面对这样的需求,我们应该如何处理,并希望由此逐步引出装饰器模式以加深理解。
需求
假设现在需要开一个奶茶店,奶茶种类繁多,如红豆奶茶,布丁奶茶,珍珠奶茶,红豆珍珠奶茶等。种类虽多,但实质上都是在奶茶中加了各种配料而已。为了简化实现,继续假设奶茶的价格根据奶茶本身加上不同配料累计计算而成。然后,根据每个客户的要求,每种奶茶又可以加糖或者加冰,加糖加冰不额外收费。
初级方案
在学习设计模式之前,或许最容易想到的方案就是继承了,即先定义奶茶类,然后再定义各种奶茶子类继承自奶茶类,考虑到以后或许还会有更多的饮品,例如咖啡,因此再定义一个饮品的抽象基类,让奶茶类继承自饮品基类,这样一来,最终设计可能会如下类图所示:
部分代码如下:
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基类。
总结
装饰器模式可以说是结构型设计模式的巅峰之作,其中设计思想十分精妙,但理解起来也确实有些困难,因此,可能还是需自己动手撸码,加深体会。