3 装饰者模式

1.绪

运行时扩展,远比编译时期的继承威力更大。本章可以成为“给爱用继承的人一个全新的设计眼界”。

本章将再度讨论典型的继承滥用问题。本章中将讲解如何使用对象组合的方式,做到运行时装饰类。一旦熟悉了装饰的技巧,则能够在不修改任何底层代码的情况下,给对象赋予新的职责。

2.应用背景-问题引入

咖啡店的故事:一家快速扩张的咖啡连锁店准备更新订单系统,以合乎他们的饮料供应要求。

原先的类设计

3 装饰者模式

购买咖啡时,可以要求在其中加入各种调料,例如:蒸奶(Steamed Milk)、豆浆(Soy)、摩卡(Mocha,也就是巧克力风味)或者覆盖奶泡。咖啡店会根据加入的调料收取不同的费用,因此订单系统必须考虑到这些调料部分。

这是该咖啡店的第一个尝试:

3 装饰者模式

 很明显,这制造了一个维护噩梦。因为如果某个调料的价格上涨或者需要新增一种调料,则会导致维护困难。

书上的初步尝试:

3 装饰者模式

 现在加入子类,每个类就代表菜单上的一种饮料:

3 装饰者模式

 这样改变带来的影响

当一些需求或者因素改变时将会影响这个设计:

  1. 调料价钱改变会使我们改变现有代码
  2. 一旦出现新的调料,则需要加上新的方法,则改变超类中的cost()方法
  3. 以后可能会开发出新饮料,对这些饮料而言(例如,冰茶),某些调料可能并不适合,但是在这个设计方式中,Tea(茶)子类仍将继承那些不适合的方法,例如,hasWhip()(加奶泡)。
  4. 万一顾客想要双倍摩卡咖啡,则对那么办?

关于组合和委托

虽然继承威力强大,但是继承不总是能够实现最有弹性和最好维护的设计。

利用组合和委托可以在运行时具有继承行为的效果。

利用继承设计子类的行为,是在编译时静态决定的,而且所有的子类都会继承到相同的行为。然而,如果能够利用组合的做法扩展对象的行为,就可以在运行时动态地进行扩展。则可以利用此技巧把多个新职责,甚至是设计超类时还没想到的职责加在对象上,而且不需要修改原来的代码。

利用组合维护代码,能够通过动态地组合对象,写新的代码添加新功能,而无需修改现有代码,既然没有改变现有代码,那么引进Bug或者产生意外副作用的机会将大幅度减少。

3.开放-关闭原则

设计原则:类应该对扩展开放,对修改关闭。

我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可搭配新的行为。实现这一目标的好处:具有弹性可以应对改变,可以接受新的功能来应对改变的需求。

相关问答:

Q1:对扩展开放,对修改关闭,听起来很矛盾,在设计的时候该如何兼顾?

A1:有一些聪明的OO技巧,允许系统在不修改代码的情况下,进行功能扩展。比如观察者模式,通过加入新的观察者,我们可以在任何时间扩展Subject(主题),而且不需要向主体中添加代码。以后,还会看到更多的扩展行为的其他OO设计技巧。

Q2:如何将某件东西设计成可以扩展,又禁止修改?

A2:在本章将使用装饰者模式的一个好例子,完全遵循开放-关闭原则。

Q3:如何设计的每个部分都遵循开放-关闭原则?

A3:通常,是办不到的,要让OO设计同时具备开放性和关闭性,又不修改现有的代码,需要花费许多时间和努力。一般来说,不会把设计的每个部分都这么设计,即便做到了,也可能是一种浪费。遵循开放-关闭原则,通常会引入新的抽象层次,增加代码的复杂度。那么需要将注意力集中在设计中最有可能改变的地方,然后应用开放-关闭原则。

4.认识装饰者模式

通过问题引入已经得知,无法利用继承来完全解决问题,遇到的问题有:类数量爆炸、设计死板,以及基类加入的新功能并不适用于所有的子类。

因此,在这里要采用不一样的做法:要以饮料为主题,然后在运行时以调料来“装饰”(decorate)饮料。比如,如果顾客想要摩卡和奶泡深焙咖啡,那么需要做的是:

  1.  拿一个深焙咖啡(DarkRoast)对象
  2. 以摩卡(Mocha)对象装饰他
  3. 以奶泡(Whip)对象装饰他
  4. 调用cost()方法,并依赖委托(delegate)将调料的价钱加上去

以装饰者构建饮料订单

第一步:以DarkRoast对象开始

3 装饰者模式

 第二步:顾客下你给要摩卡(Mocha),所以建立一个Mocha对象,并用它将DarkRoast对象包(wrap)起来。

3 装饰者模式

 第三步:顾客也想要奶泡(Whip),所以需要建立一个Whip装饰者,并用它将Mocha对象包起来。别忘了,DarkRoast继承自Beverage,且有一个cost()方法,用来计算饮料价钱。

3 装饰者模式

 第四步:到为顾客算钱的时候了。通过调用最外圈装饰者(Whip)的cost()就可以办得到。Whip的cost()会先委托它装饰的对象(也就是Mocha)计算出价钱,然后再加上奶泡的价钱。

3 装饰者模式

 到此则装饰结束。这就是目前所知道的一切:

  1. 装饰者和被装饰对象有相同的超类型。
  2. 可以用一个或多个装饰者包装一个对象
  3. 既然装饰者和被装饰对象有相同的超类型,所以在任何需要原始对象(被包装的)场合,可以用装饰过的对象代替它。
  4. 装饰者可以在所委托被装饰者的行为之前与/或之后,加上自己的行为,以达到特定的目的。
  5. 对象可以在任何时候被装饰,所以可以在运行时动态地,不限量地用所喜欢的装饰者来装饰对象。

接下来就看看装饰者模式的定义,并写一些代码,了解它到底是怎么工作的。

5.定义装饰者模式

5.1 装饰者模式的说明

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

装饰者模式的类图:

需要知道的是:每个装饰者都包装一个组件,也就是说,装饰者有一个实例变量以保存某个Component的引用。装饰者本身也是继承自Component的。

3 装饰者模式

 

 5.2 装饰饮料

将咖啡店的饮料应用在这个框架上:

3 装饰者模式

 组件其实就是被装饰者

 5.3 关于继承和组合之间,混淆的观念

通过类图可知,CondimentDecorator扩展自Beverage类,这用到了继承。这么做的重点在于:装饰者和被装饰者必须是一样的类型,即拥有共同的超类,这是相当关键的,因为我们利用继承达到“类型匹配”,而不是利用继承获得“行为”。

装饰者需要和被装饰者(被包装的组件)有相同的“接口”,因为装饰者必须能取代被装饰者,但是行为又是从哪里来的?当我们将装饰者和组件组合时,就是在加入新的行为。所得到的新行为,并不是继承自超类,而是由组合对象得来的。继承Beverage抽象类,是为了有正确的类型,而不是继承他的行为。行为来自装饰者和基础组件,或与其他装饰者之间的组合关系。因为使用对象组合,可以把所有饮料和调料更加有弹性的加以混合与匹配,如果只是依赖继承,那么类的行为只能在编译时静态决定,即行为不是来自超类,那么就是子类覆盖后的版本。反之,利用组合,可以把装饰者混合着用,并且是在“运行时”。并且这样的话,就能够在任何时候,实现新的装饰者增加新的行为。如果依赖继承,每当需要新行为时,还得修改现有的代码。

如果需要继承的是component类型,为什么不把Beverage类设计成一个接口,而是设计成一个抽象类呢?通常装饰者模式采用抽象类,但是在JAVA中可以使用接口。尽管如此,通常我们都努力避免修改现有的代码,所以,如果抽象类运作得好好地,还是别去修改它。

6.代码实现

6.1 基类和装饰者类的实现

首先从Beverage类下手,这不需要改变咖啡店原始的设计:

public abstract class Beverage {

    String description="Unkonwn Beverage";

    public String getDescription() {
        return description;
    }

    public abstract double cost();
}

 Beverage很简单。同样来实现调料的抽象类Condiment,,也就是装饰者类:

//首先,必须让CondimentDecorator能够取代Beverage,所以CondimentDecorator扩展自Beverage类
public abstract class CondimentDecorator extends Beverage{

    //所有的调料装饰者都必须重新实现getDescription()方法。
    @Override
    public abstract String getDescription();

}

6.2 写饮料的代码

基类已经建立完成了,则开始实现一些饮料。

首先创建 浓缩咖啡(Espresso)开始,我们需要为具体的饮料设置描述,而且还必须实现cost()方法。

//首先,让Espresso扩展自Beverage类,因为Espresso是一种饮料
public class Espresso extends Beverage{

    //为了要设置饮料的描述,我们写了一个构造器,记住:description实例变量继承自Beverage。
    public Espresso() {
        description="Espresso";
    }

    //最后,需要计算Espresso的价钱,现在不管调料的价钱,直接把Espresso的价格返回即可
    @Override
    public double cost() {
        return 1.99;
    }
}

同样的,编写HouseBlend的相关代码:

public class HouseBlend extends Beverage{

    public HouseBlend() {
        description="HouseBlend";
    }

    @Override
    public double cost() {
        return .89;
    }
}

6.3 写调料代码

上面完成了抽象组件,具体组件,也有了抽象装饰者。现在,就来实现具体装饰者。

首先编写摩卡Mohca类:

//Mocha是一个装饰者,所以让他扩展自 CondimentDecorator
public class Mocha  extends CondimentDecorator{
    //要让Mocha能够引用一个Beverage,做法如下
    //1.用一个实例变量记录饮料,也就是被装饰者
    //2.想办法让被装饰者(饮料)被记录到实例变量中。这里的做法是:
    //把饮料当做构造器的餐宿,再由构造器将其饮料记录在实例变量中
    Beverage beverage;

    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription()+",Mocha";
    }

    //要计算带Mocha饮料的价格。首先把调用委托给被装饰对象,以计算价钱,然后再加上Mocha的价钱,得到最后结果
    @Override
    public double cost() {
        return .20+beverage.cost();
    }
}

同理写下 Soy和Whip调料的代码:

public class Whip extends CondimentDecorator {

    Beverage beverage;

    public Whip(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription()+",Whip";
    }


    @Override
    public double cost() {
        return .10+beverage.cost();
    }
}

public class Soy extends CondimentDecorator {

    Beverage beverage;

    public Soy(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription()+",Soy";
    }


    @Override
    public double cost() {
        return .15+beverage.cost();
    }
}

6.4 供应咖啡

编写测试代码

public class StarbuzzCoffee {

    public static void main(String[] args) {
        Beverage beverage=new Espresso();
        beverage=new Mocha(beverage);
        beverage=new Whip(beverage);
        System.out.println(beverage.getDescription()+" $ "+beverage.cost());
    }
}

实验结果:

3 装饰者模式

 

上一篇:Python学习之字符串常用方法


下一篇:746. 使用最小花费爬楼梯