2020-08-09

Head First 设计模式之第三章——装饰者模式

内容回顾

在第三章中,作者主要讲述了这样一个应用场景:咖啡店要做一个咖啡的订单系统,根据饮料及其附加的调味料来计算出饮料的价格。

对于这个系统,有一定OO基础的菜鸟程序员很容易想到,直接写一个饮料的基类(如Beverage),基类中定义一个函数cost(),针对具体的饮料,分别定义Beverage的子类,如HouseBlend, Decaf等等,然后分别实现对应的cost()函数,客户点哪种饮料,就直接创建该饮料类的对象,然后直接调用其cost()函数,就可以直接得到该饮料的价格。但是,此方案是很糟糕的。按照上述的方案的思路,一种饮料创建一个饮料的子类,那咖啡需要定义一个子类,咖啡+牛奶也定义一个子类,咖啡+牛奶+糖也定义成一个子类,那样就会出现一种类似“类爆炸”的现象,如下图所示。如此多的类,对开发者而言,需要写非常多的重复代码,对于维护者而言,维护起来简直就是灾难,所以这个方案是十分脑残的。
2020-08-09

对于一些对面向对象有一知半解的开发者而言,他们用OO来实践的时候,无非就是封装、继承与多态,但他们往往会忽略组合的力量。如上述的方案,从父类中派生子类,然后在子类中添加新的特性,这是一种在编译时对对象的行为进行扩展。其实使用组合也可以实现对象行为的扩展,不同的是,它在运行时对对象的行为进行扩展,后者具有更高的灵活性,同时也更加符合OO设计中的开放-封闭原则。

回到饮料的订单系统这个问题上,我们作进一步的分析:订单系统中,饮料相当于部件,而调味料(如牛奶、糖等等)相当于对饮料这个部分的装饰,为了解决类爆炸的问题,我们可不可以这样:先定义若干个饮料的对象,再定义若干个调味料对象,然后饮料与调味料在运行时进行组合,那样就可以组合出非常多的饮料对象,那样就实现用很少的类实现非常多的饮料对象了。上述的思路就是装饰者模式的思路。

装饰者模式概述

装饰者模式的类图如下图所示,首先从部件类(Component)中派生出具体的部件类(ConcreteComponent)与装饰器类(Decorator),具体部件类就是要被装饰的对象了,从Decorator中派生出若干种类的装饰器,有各个具体的装饰器类中,都有(Has-a)部件基类的成员变量,这个成员变量所表示的部件对象就是要装饰的对象了。例如,创建一个ConcreteComponent类对象,将其传到一个ConcreteDecoratorA类对象中,部件类与装饰器类均有methodA()与methodB()方法,部件类被具体的装饰器类装饰后,它依然还是Component类,在调用methodA()(或methodB())时,被装饰后的对象会在原来部件的methodA()的基础上,添加装饰器的行为,如下图所示,在调用ConcreteDecoratorA类的methodA方法时,会在wrappedObj对象的methodA或其他状态的基础上,添加一些新的行为,以达到装饰的效果。所以装饰器模式的核心就是:

  1. 装饰器类与具体部件类均继承(实现)自部件基类;
  2. 具体的装饰器类中有(Has-A)部件类型的成员变量;
  3. 具体的装饰器类在实现部件的接口(如methodA() or methodB())时,会在原来具体部件的对应的接口实现的基础上,添加一些新的行为,从而达到装饰的目的。
    2020-08-09

针对上述说到的订单系统,最终的设计如下:

  1. Beverage是饮料的父类,具体的饮料类与调味料类都是它的子类;
  2. 从Beverage类派生了四种饮料类;
  3. 从Beverage类派生出具体的装饰器基类,然后从这个基类中派生出四种调味料类,每个类中均有(Has-A)一个Beverage类的成员变量,这个就是要被装饰的饮料对象,在调用调味料类的cost函数时,先调用饮料类的cost,然后加上该调味料的价格,从而算出饮料+调味料的总价格。
    2020-08-09
    在使用,先创建一个饮料类对象,如创建一个DarkRoast对象darkRoast,然后创建一个装饰器Milk对象darkRoastWithMilk,将darkRoast传入到后者中,如果想再对该对象包装一层Whip,那可以创建一个Whip对象darkRoastWithMilkWhip,将darkRoastWithMilk对象传入到darkRoastWithMilkWhip对象中,这样在计算价格时,就可以实现DarkRoast价格+Milk价格+Whip价格的目的。如下图所示。
    2020-08-09

总结

装饰器模式的实际就是使用组合实现在运行时实现对象的行为扩展,且很符合开发-封闭原则,即对扩展开发,对修改封闭。但是装饰者对象有一个不好的地方,即装饰器模式会导致程序中有很多小对象,如果过度使用,可能会增加程序的复杂度。