设计模式之装饰者(Decorate)模式

什么是装饰者模式?

  装饰者模式,是面向对象编程领域中,一种动态地往一个类中添加新的行为的设计模式。就功能而言,装饰者模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能。

认识装饰者模式

从一个例子准备了解装饰者模式

  虽然笔者并不喜欢喝奶茶,但奶茶店在街道上随处可见。假如你要开一家奶茶店,会怎么设计奶茶相关的类呢?

最初的设计

  下面为奶茶中涉及到的类的设计:


  购买奶茶时,光有奶茶还不够,还可以要求在里面加入各种好吃的配料,如黑珍珠(BlackPearl),冰块(IceCake),冰淇凌(IceCream),芒果块(MongoBlock)等等。奶茶店会根据所加入的配料收取不同的费用,所以订单系统必须考虑到这些配料部分。
  因此原先设计的要适当变化:

  很明显,这样设计会为我们后期维护带来相当大的困难,如果黑珍珠的价格上涨,怎么办?新增一种新的布丁配料时又怎么办?新手才会这么设计。
  我们不应该设计这么多的类,如果使用实例变量和继承,就可以追踪这些配料。

改进后的奶茶类

  因此,我们重新改造一下,将MilkyTea基类加上实例变量,代表是否加上配料(黑珍珠、冰块、冰淇凌、芒果块等等):

  MilkyTea基类实现的代码:

public class MilkyTea {
    protected String description;

    private double blackPearlCost = 1.0;
    private double iceCakeCost = 1.0;
    private double iceCreamCost = 2.0;
    private double MongoBlackCost = 3.0;

    private boolean blackPearl;
    private boolean iceCake;
    private boolean iceCream;
    private boolean MongoBlack;

    public double cost() {
        double condimentCost = 0.0;
        if (isBlackPearl()) {
            condimentCost += blackPearlCost;
        }
        if (isIceCake()) {
            condimentCost += iceCakeCost;
        }
        if (isIceCream()) {
            condimentCost += iceCreamCost;
        }
        if (isMongoBlack()) {
            condimentCost += MongoBlackCost;
        }

        return condimentCost;
    }


    public String getDescription() {
        return description;
    }

    public boolean isBlackPearl() {
        return blackPearl;
    }

    public void setBlackPearl(boolean blackPearl) {
        this.blackPearl = blackPearl;
    }

    public boolean isIceCake() {
        return iceCake;
    }

    public void setIceCake(boolean iceCake) {
        this.iceCake = iceCake;
    }

    public boolean isIceCream() {
        return iceCream;
    }

    public void setIceCream(boolean iceCream) {
        this.iceCream = iceCream;
    }

    public boolean isMongoBlack() {
        return MongoBlack;
    }

    public void setMongoBlack(boolean mongoBlack) {
        MongoBlack = mongoBlack;
    }
}

  具体实现的2种奶茶类:

//纯苹果奶茶什么也没加
public class AppleMilkyTea extends MilkyTea {
    public AppleMilkyTea() {
        description = "好喝的苹果奶茶";
    }
    
    //就花了2块钱
    @Override
    public double cost() {
        return 2.0 + super.cost();
    }
}

//苹果奶茶加了黑珍珠和冰块
public class AppleMilkyTeaWithBlackPearlAndIceCake extends MilkyTea {
    public AppleMilkyTeaWithBlackPearlAndIceCake() {
        setBlackPearl(true);
        setIceCake(true);
        description = "好喝的苹果奶茶还加了黑珍珠和冰块";
    }
    //花了2块钱加上配料的钱
    @Override
    public double cost() {
        return 2.0 + super.cost();
    }
}

  虽然改进后的类比原来更具有弹性了,但是当有些需求或因素改变时还是会影响这个设计。
  比如配料价钱的改变会使我们更改现有代码。一旦出现新的配料,我们就需要加上新的方法,并改变超类中的cost()方法。而且以后可能会有新类型的奶茶,对某些奶茶而言,和有些配料是不能混在一起吃的(比如菠萝奶茶和芒果配料是不能一起吃的)。
  但是在这个设计方式中,奶茶的子类仍将继承那些不合适的方法(虽然这些方法可能不会使用)。因此,我们需要使用装饰者模式。

着手解决问题

  从前面我们知道利用继承无法完全解决问题:类数量爆炸、设计死板,以及超类加入的新功能并不适用于所有的子类。
  所以,在这里我们要采用不一样的做法:以奶茶为主体,然后在运行时以配料来“装饰”奶茶。比如,顾客想要蓝莓奶茶加黑珍珠和芒果块,那么需要:
  拿一个蓝莓奶茶对象
  以黑珍珠对象装饰它
  以芒果块对象装饰它
  调用cost()方法,并依赖委托将配料的价钱加上去。
  那么如何“装饰”一个对象,“委托”又如何与此搭配使用?我们可以把装饰者对象当成“包装者”。

以装饰者构造奶茶订单

  ①以BlueberryMilkyTea对象为开始(蓝莓奶茶继承自奶茶,且有一个计算价钱的cost()方法):

  ②顾客还想要黑珍珠(BlackPearl),所以建立一个BlackPearl对象,并用它将BlueberryMilkyTea对象包装起来(BlackPearl对象是一个装饰者,它的类型“反映”了它所装饰的MilkyTea对象。“反映”即指两者类型一致。通过多态可以把BlackPearl所包裹的任何类型的MilkyTea当成是MilkyTea)。

  ③顾客又想要芒果块(MongoBlack),所以需要建立一个MongoBlack对象,并用它把BlackPearl对象包装起来

  ④现在为顾客计算花费的价钱。通过调用最外圈装饰者(MongoBlack)的cost()就可以办得到。MongoBlackcost()会先委托它装饰的对象也就是BlackPearl计算出价钱,再加上芒果块的价钱,具体如下图:


  从上面我们可以知道:

  • 装饰者和被装饰对象有相同的父类
  • 可以用一个或多个装饰者包装一个对象
  • 既然装饰者和被装饰对象有相同的父类,那么可以在任何需要原始对象的场合,用装饰过的对象代替它。
  • 装饰者可以在所委托被装饰者的行为之前或之后,加上自己的行为,以达到特定目的
  • 对象可以在任何时候被装饰,所以可以在运行时动态的、不限量地用你喜欢的装饰者来装饰对象

定义装饰者模式

   装饰者模式动态地将责任附加到对象上。 若要扩展功能,装饰者提供了比继承更有弹性 的替代方案。

  下面为装饰者的类图,我们后面会套用此结构:


简化的类图

装饰者模式改进的例子

  现在让我们的奶茶符合上面定义的结构,类图如下:


  从类图我们看到,CondimentDecorator扩展自MilkyTea类,用到了继承。这么做的重点在于,装饰者和被装饰者必须是一样的类型,也就是有共同的父类,这是相当关键的地方。这里的继承是达到“类型匹配”,而不是利用继承获得“行为”。
  那么行为又是从哪里来的呢?
  当我们将装饰者和组件组合时,就是在加入新的行为。所得到的新行为,并不是继承自父类,而是由组合对象得来的。也就是说,继承MilkyTea类,是为了有正确的类型,而不是继承它的行为。行为来自装饰者和基础组件,或与其他装饰者之间的组合关系。
  因为使用对象组合,就可以把所有奶茶和配料更有弹性地加以混合和匹配,非常方便。如果依赖继承,那么类的行为只能在编译时静态决定,行为不是来自父类,就是子类覆盖后的版本。反之,利用组合,可以把装饰者混合使用,而且是在“运行时”!!!
  为什么不把MilkyTea设计成一个接口,而是抽象类呢?
  通常装饰者模式是采用抽象类,但是在Java中可以使用接口。尽管如此,我们都努力避免修改现有的代码,所以,如果抽象类运作的好好地,还是别去修改它。

代码具体实现

  现在开始正在设计代码了,首先从MilkyTea切入:

public abstract class MilkyTea {
    String description = "白开水状态的奶茶。。。";
    
    public String getDescription(){
        return description;
    }
    
    public abstract double cost();
}

  然后实现CondimentDecorator抽象类,也就是装饰者类,黑珍珠冰块等配料都继承该类:

public abstract class CondimentDecorator extends MilkyTea {
   //所以的配料都必须重新实现该方法
   public abstract String getDescription();
}

  接着开始写一些奶茶类吧,需要将具体的奶茶描述一下哦(类别描述和价钱花费),好让顾客选择,代码如下:

//苹果奶茶
public class AppleMilkyTea extends MilkyTea {
    
    //描述奶茶具体的种类
    public AppleMilkyTea() {
        description = "这是苹果奶茶";
    }
    
    //奶茶的价钱花费
    @Override
    public double cost() {
        return 1.0;
    }
}

//蓝莓奶茶
public class BlueberryMilkyTea extends MilkyTea {

    public BlueberryMilkyTea() {
        description = "这是蓝莓奶茶";
    }

    @Override
    public double cost() {
        return 3.0;
    }
}

  完成了抽象组件(MIlkyTea),有了具体组件(AppleMilkyTeaBlueberryMilkyTea),也有了装饰者(CondimentDecorator),紧接着就是实现具体装饰者配料类了,顾客说:往我的奶茶里加点黑珍珠和冰块。好嘞:

//黑珍珠
public class BlackPearl extends CondimentDecorator {
    //用一个实例变量记录奶茶,也就是被装饰者
    MilkyTea milkyTea;

    //想办法让被装饰者被记录到实例变量中,把奶茶当作构造器参数传入即可,再记录给实例变量
    public BlackPearl(MilkyTea milkyTea) {
        this.milkyTea = milkyTea;
    }

    /**
     * 不仅仅描述了奶茶,我们还得完整地描述配料
     * 因此首先利用委托的方法,在CondimentDecorator得到一个描述
     * 再在其子类附加具体的描述
     * @return
     */
    @Override
    public String getDescription() {
        return milkyTea.getDescription() + ",并且加了黑珍珠";
    }

    //自然也要计算配料的价钱
    @Override
    public double cost() {
        return 1.0 + milkyTea.cost();
    }
}

//冰块
public class IceCake extends CondimentDecorator {

    MilkyTea milkyTea;

    public IceCake(MilkyTea milkyTea) {
        this.milkyTea = milkyTea;
    }

    @Override
    public String getDescription() {
        return milkyTea.getDescription() + ",并且加了冰块";
    }

    @Override
    public double cost() {
        return 1.0 + milkyTea.cost();
    }
}

  最后就是测试类了:

         //一个什么都没加的苹果奶茶
        MilkyTea milkyTea = new AppleMilkyTea();
        System.out.println(milkyTea.getDescription() + ",价钱人民币" + milkyTea.cost() + "块");

        //创建一杯蓝莓奶茶
        MilkyTea milkyTea1 = new BlueberryMilkyTea();
        //用黑珍珠装饰它
        milkyTea1 = new BlackPearl(milkyTea1);
        //再用冰块装饰它
        milkyTea1 = new IceCake(milkyTea1);
        System.out.println(milkyTea1.getDescription() + ",价钱人民币" + milkyTea1.cost() + "块");

  测试类运行结果如下:

这是苹果奶茶,价钱人民币1.0块
这是蓝莓奶茶,并且加了黑珍珠,并且加了冰块,价钱人民币5.0块

装饰者模式的缺点

  硬币有正反两面,装饰者模式也有一个“缺点”:利用装饰者模式,常常造成设计中有大量的小类,数量实在太多,可能会造成使用相关API(如java.io)程序员的困扰,不过知道了装饰者的工作原理,以后就能很容易地辨别出它们的装饰者类是如何组织的,以方便用包装方式取得想要的行为。

Java中的装饰者:I/O类

  java.io类非常多,其中许多类都是装饰者。下面是一个典型的对象集合,用装饰者来将功能结合起来,以读取文件数据:


  BufferedInputStreamLineNumberInputStream都扩展自FilterInputStream,而FilterInputStream是一个抽象的装饰类。
  让我们查看一下各种I/O类之间的关系:

  可以发现,其和奶茶店的设计相比其实并没有多大的差异。将java.ioAPI范围缩小,可以容易的查看它的文件,并组合各种“输入”流装饰者来符合我们的用途。
  类似地,“输出”流的设计方式也是一样的,而且字符流的设计和字节流的设计也相当类似(有一点小差异),知道装饰者模式之后,可以更好地理解这些类。

开闭(Open Closed)原则

  开放-关闭原则:类应该对扩展开放,对修改关闭。
  我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可搭配 新的行为。如能实现这样的目标,有什么好处呢?这样的设计具有弹性,可以应对改变,可以接受新的功能来应对改变的需求。
  对扩展开放,对修改关闭?乍听之 下,的确感到矛盾,毕竟,越难修改的事 物,就越难以扩展。但是,有一些聪明的OO技巧,允许系统在不修改代码的情况下,进行功能扩展。 如观察者模式,通过加 入新的观察者,我们可以在任何时候扩展 Subject(主题),而且不需向主题中添加代码。
  装饰者模式完全遵循开放-关闭原则
  我们不需要让设计的每个部分都遵循开放-关闭原则,通常办不到,要让OO设计同时具备开放性和关闭性,又不修改现有的代码,需要花费许多时间和努力。一 般来说,我们实在没有闲工夫把设计的每 个部分都这么设计(而且,就算做得到, 也可能只是一种浪费)。遵循开放-关闭原 则,通常会引入新的抽象层次,增加代码的复杂度。只需要把注意力集中在设计中最有可能改变的地方,然后应用开放-关闭原则即可。

参考资料

《HeadFirst设计模式》

你可能感兴趣的:(设计模式之装饰者(Decorate)模式)