3 装饰者模式

​​​​​​​

1.绪

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

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

2.应用背景-问题引入

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

原先的类设计

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

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

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

书上的初步尝试:

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

 这样改变带来的影响

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

  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对象开始

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

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

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

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

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

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

5.定义装饰者模式

5.1 装饰者模式的说明

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

装饰者模式的类图:

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

 5.2 装饰饮料

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

 组件其实就是被装饰者

 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());
    }
}

实验结果:

7.真实世界的装饰者:Java I/O

java.io包内的类太多了。现在已经知道装饰者模式,这其中许多类都是装饰者。下面就是一个典型的对象集合,用装饰者来将功能结合起来,已读取文件数据:

 BufferedInputStream及LineNumberInputStream都扩展自FilterInputStream,而FilterInputStream是一个抽象的装饰类。

装饰Java.io类:

上图可知,FileInputStream、StringBufferInputStream、ByteArrayInputStream都是可以被装饰者抱起来的具体组件。 

FilterInputStream是一个抽象装饰者。

对比这个,可以和咖啡店的设计相比,java.io其实没有多大的差异。我们把java.io API范围缩小,让你容易查看他的文件,并组合各种“输入”流装饰者来符合你的用途。同样,“输出”流的设计方式也是一样的。并且Reader\Writer流(作为基于字符数据的输入输出)和输入流\输出流的类相当类似。

但是Java I/O也引出装饰者模式的一个“缺点”:利用装饰者模式,会造成设计中有大量的小类,数量实在太多,可能会造成使用此API程序员的困扰。但是,现在已经了解了装饰者的工作原理,以后当使用别人的大量装饰的API时,就可以很容易地辨别出他们的装饰者类是如何组织的,以方便用包装方式取得想要的行为。

8.编写自己的Java I/O装饰者

问题需求:编写一个装饰者,把输入流的所有大写字母转成小写。举例:“I Know the Decorator Pattern therefore I RULE!”,装饰者会将它转成“i know the decorator pattern therefore i rule!”

首先编写装饰者对应的类:

public class LowerCaseInputStream extends FilterInputStream {

    public LowerCaseInputStream(InputStream in) {
        super(in);
    }

    //必须实现两个read()方法,一个针对字节,一个针对字节数组,把每个是大写字符的字节(每个代表一个字符)转换成小写
    @Override
    public int read() throws IOException{
        int c=super.read();
        return (c==-1 ? c:Character.toLowerCase((char)c));
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int result=super.read(b, off, len);
        for (int i=off;i<off+result;i++){
            b[i]=(byte)Character.toLowerCase((char)b[i]);
        }
        return result;
    }
}

测试代码:

public class InputTest {

    public static void main(String[] args) throws IOException{
        int c;
        try{
            InputStream in=new LowerCaseInputStream(
                    new BufferedInputStream(
                            new FileInputStream("test.txt")
                    ));
            while((c=in.read())>=0){
                System.out.print((char) c);
            }

        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

测试输出:

9.设计箱内的工具

截止到当前章节,已经有的内容与要点:

  1. 继承属于扩展形式之一,但不见得是达到弹性设计的最佳方式。
  2. 在我们的设计中,应该允许行为可以被扩展,而无须修改现有的代码。
  3. 组合和委托可用于在运行时动态地加上新的行为。
  4. 除了继承,装饰者模式也可以让我们扩展行为。
  5. 装饰者模式意味着一群装饰者类,这些类用来包装具体组件。
  6. 装饰者类反映出被装饰的组件类型(事实上,他们具有相同的类型,都经过接口或继承实现)。
  7. 装饰者可以在被装饰者的行为前面与/或后面加上自己的行为,甚至将被装饰者的行为整个取代掉,而达到特定的目的。
  8. 你可以用无数个装饰者包装一个组件。
  9. 装饰者一般对组件的客户是透明的,除非客户程序依赖于组件的具体类型
  10. 装饰者会导致设计中出现许多小对象,如果过度使用,会让程序变得很复杂

装饰者会导致设计中出现许多小对象,如果过度使用,会让程序变得很复杂。
 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值