学习:设计模式之装饰者模式

本文来自www.lanttor.org


星巴兹咖啡的扩张

“给爱用继承的人一个全新的设计眼界”:学习如何使用对象组合的方式,做到在运行时装饰类。为什么呢?一旦熟悉了装饰的技巧,你将能够在不修改任何底层代码的情况下,给你的对象赋予新的职责。

1 咖啡饮料的原先设计

基础类Beverage设计:

购买咖啡,还要求加入各种调料。例如奶,豆浆,摩卡等。所以订单系统要考虑到这些调料的价格。

 

使用继承的方式尝试设计:

 

这种极端的继承设计,导致了类的爆炸。(不可取)

2 咖啡饮料设计的更改

利用实例变量和继承,追踪这些调料。在Beverage基类里,加上实例变量代表是否加上调料:

然后继承Beverage基类,实现具体的子类(每个子类代表菜单上的一种饮料):

这种做法,只需要5个类。而且结构也清晰。但存在的潜在的问题有:

  • 调料价钱的改变会使我们更改现有的代码。
  • 一旦出现新的调料,我们需要加上新的方法,并改变超类中的cost()方法。
  • 以后会开发新的饮料,某些调料可能不合适。但是子类仍会继承这些不合适的方法。
  • 万一顾客需要双倍摩卡咖啡,怎么办?

 

更进一步的想法:

利用组合(composition)和委托(delegation)可以在运行时具有继承行为的效果。利用继承设计子类的行为,是在编译时静态决定的,而且所有的子类都会继承到相同的行为。然而,如果能够利用组合的做法扩展对象的行为,就可以在运行时进行动态扩展。

 

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

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

3 认识装饰者模式

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

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

 

3.1 以装饰者构造饮料订单

1. 以DarkRoast对象开始

2. 顾客想要摩卡,所以建立Mocha对象,并用它将DarkRoast对象包(wrap)起来

3. 顾客想要奶泡,所以需要建立一个Whip装饰者,并用它将Mocha对象包起来。

 

4. 为顾客算钱

3.2 对装饰者模式的认识

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

4 定义装饰者模式

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

 

咖啡饮料新的设计框架:

5 星巴兹咖啡饮料代码

5.1 Beveraga类

Beverage.java:

package headfirst.decorator.starbuzz;

public abstract class Beverage {

    String description = "Unknown Beverage";

    public String getDescription() {

        return description;

    }

    public abstract double cost();

}

 

 

Beverage类是一个抽象类。getDescription()已经实现了,但是cost()必须在子类实现。

5.2 调料装饰CondimentDecorator类

CondimentDecorator.java:

package headfirst.decorator.starbuzz;

public abstract class CondimentDecorator extends Beverage {

    public abstract String getDescription();

}

 

所有的调料装饰者都必须重新实现getDescription()方法。

5.3 饮料浓缩咖啡类(Espresso)

Espresso.java:

package headfirst.decorator.starbuzz;

public class Espresso extends Beverage {

    public Espresso() {

        description = "Espresso";

    }

    public double cost() {

        return 1.99;

    }

}

为了设置饮料的描述,我们写了一个构造器。description实例变量继承自Beverage。

5.4 其他饮料类

家常咖啡类HouseBlend.java:

package headfirst.decorator.starbuzz;

public class HouseBlend extends Beverage {

    public HouseBlend() {

        description = "House Blend Coffee";

    }

    public double cost() {

        return .89;

    }

}

 

深焙咖啡类DarkRoast.java:

package headfirst.decorator.starbuzz;

public class DarkRoast extends Beverage {

    public DarkRoast() {

        description = "Dark Roast Coffee";

    }

    public double cost() {

        return .99;

    }

}

 

低咖啡因类Decaf.java:

package headfirst.decorator.starbuzz;

public class Decaf extends Beverage {

    public Decaf() {

        description = "Decaf Coffee";

    }

    public double cost() {

        return 1.05;

    }

}

5.5 调料Mocha类

Mocha.java:

package headfirst.decorator.starbuzz;

public class Mocha extends CondimentDecorator {

    Beverage beverage;

    public Mocha(Beverage beverage) {

        this.beverage = beverage;

    }

    public String getDescription() {

        return beverage.getDescription() + ", Mocha";

    }

    public double cost() {

        return .20 + beverage.cost();

    }

}

魔卡是一个装饰者,所以让它扩展自CondimentDecorator。

 

要让摩卡能够引用一个Beverage,做法如下:

  • 用一个实例变量记录饮料,也就是被装饰者。
  • 想办法让被装饰者(饮料)被记录到实例变量中。这里的做法是把饮料当作构造器的参数,再由构造器将此饮料记录在实例变量中。

 

重新实现getDescription():不仅描述饮料,而且要描述装饰的调料。

重新实现cost(),首先把调用委托给装饰对象,以计算价钱,然后再加上Mocha的价钱,得到最后结果。

5.6 其他调料类

Milk.java:

package headfirst.decorator.starbuzz;

public class Milk extends CondimentDecorator {

    Beverage beverage;

    public Milk(Beverage beverage) {

        this.beverage = beverage;

    }

    public String getDescription() {

        return beverage.getDescription() + ", Milk";

    }

    public double cost() {

        return .10 + beverage.cost();

    }

}

 

Soy.java:

package headfirst.decorator.starbuzz;

public class Soy extends CondimentDecorator {

    Beverage beverage;

    public Soy(Beverage beverage) {

        this.beverage = beverage;

    }

    public String getDescription() {

        return beverage.getDescription() + ", Soy";

    }

    public double cost() {

        return .15 + beverage.cost();

    }

}

 

Whip.java:

package headfirst.decorator.starbuzz;

public class Whip extends CondimentDecorator {

    Beverage beverage;

    public Whip(Beverage beverage) {

        this.beverage = beverage;

    }

    public String getDescription() {

        return beverage.getDescription() + ", Whip";

    }

    public double cost() {

        return .10 + beverage.cost();

    }

}

5.7 测试代码

StarBuzzCoffee.java:

package headfirst.decorator.starbuzz;

public class StarbuzzCoffee {

    public static void main(String args[]) {

        Beverage beverage = new Espresso();

        System.out.println(beverage.getDescription()

                + " $" + beverage.cost());

        Beverage beverage2 = new DarkRoast();

        beverage2 = new Mocha(beverage2);

        beverage2 = new Mocha(beverage2);

        beverage2 = new Whip(beverage2);

        System.out.println(beverage2.getDescription()

                + " $" + beverage2.cost());

        Beverage beverage3 = new HouseBlend();

        beverage3 = new Soy(beverage3);

        beverage3 = new Mocha(beverage3);

        beverage3 = new Whip(beverage3);

        System.out.println(beverage3.getDescription()

                + " $" + beverage3.cost());

    }

}

第一杯咖啡,浓缩咖啡,不需要调料;

第二杯咖啡,深焙咖啡,两份摩卡,一份奶泡;

第三杯咖啡,家常咖啡,一份豆浆,一份魔卡,一份奶泡。

 

测试结果:

Espresso $1.99

Dark Roast Coffee, Mocha, Mocha, Whip $1.49

House Blend Coffee, Soy, Mocha, Whip $1.34

 

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

下面是一个典型的对象集合,用装饰者来将功能结合起来,以读取文件数据:

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

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

编写一个装饰者,把输入流的所有大写字符转成小写。

 

LowerCaseInputStream.java:

package headfirst.decorator.io;

import java.io.*;

public class LowerCaseInputStream extends FilterInputStream {

    public LowerCaseInputStream(InputStream in) {

        super(in);

    }

    public int read() throws IOException {

        int c = super.read();

        return (c == -1 ? c : Character.toLowerCase((char)c));

    }

    public int read(byte[] b, int offset, int len) throws IOException {

        int result = super.read(b, offset, len);

        for (int i = offset; i < offset+result; i++) {

            b[i] = (byte)Character.toLowerCase((char)b[i]);

        }

        return result;

    }

}

 

测试代码InputTest.java:

package headfirst.decorator.io;

import java.io.*;

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

            }

            in.close();

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

设置FileInputStream,先用BufferedInputStream装饰它,再用我们崭新的LowerCaseInputStream过滤器装饰它。

只用流来读取字符,一直到文件尾端。每读一个字符,就马上将它显示出来。

7 总结

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

本文档完整的文档请参考“设计模式之装饰者模式”


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