天天看点

《Head First 设计模式》模式3——装饰者模式为Starbuzz开发订单系统以装饰者构造饮料订单重构后的订单系统实现装饰者模式装饰者模式应用者——Java I/O

概念什么的都是正确的废话!

所以不说废话,直接上栗子:

为Starbuzz开发订单系统

Starbuzz是一家咖啡连锁店,他们准备更新订单系统,以适应新品类的咖啡和调料的不断加入,达到供应需求。

Starbuzz改进了他们原先的系统,由饮料基类

Beverage

开始,在

Beverage

里有各种调料,

cost()

方法计算饮料所用调料的总价钱,然后由子类(比如:

HouseBlend

(综合)、

DarkRoast

(超优深焙咖啡豆)、

Decaf

(低咖啡因)、

Espresso

(浓缩咖啡))在覆盖基类的

cost()

,计算调料价钱和咖啡价钱的总和。

饮料基类

Beverage

的Java代码为:

public class Beverage {
    String description = "Unkown Beverage";

    private boolean milk;
    private boolean soy;
    private double milkCost;
    private double soyCost;

    public Beverage() {
    }

    public String getDescription(){
        return this.description;
    }

    public double cost() {
        double sum = ;
        if (hasMilk())
            sum += milkCost;
        if (hasSoy())
            sum += soyCost;
        return sum;
    }

    public boolean hasMilk() {
        return milk;
    }
    public boolean hasSoy() {
        return soy;
    }

    // milk、soy等调料的getter和setter方法
}
           

咖啡品种以

DarkRoast

为例:

public class DarkRoast extends Beverage {
    public DarkRoast() {
        description = "Most Excellent Dard Roast";
    }

    public double cost() {
        return  + super.cost();
    }
}
           

看起来好像可以应付所需,但是存在一些潜在问题:变化的部分(调料)一改变就涉及修改,不能动态地添加新功能!

有没有更好地办法改进呢?

以装饰者构造饮料订单

从上面的改造可以看到,当新功能出现时(添加新的调料)时,就要修改

Beverage

的代码,不能动态地组合对象以避免修改。这是不符合“开闭原则”的!

此处引出第5个设计原则:开放-关闭原则

类应该对扩展开放,对修改关闭

哎,前面的设计太死板,基类不能加入新功能以适应所有子类,太失败了!

经过思考,我们决定重新改造:以饮料为主体,在运行时以调料来“装饰”(decorate)饮料。比方说,如果顾客想要一杯深焙咖啡,加上摩卡和奶泡调料,那么做法是:

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

PS:此处的“委托”并不是类似C#语法中的delegate,而是指传递性的调用上一级的方法。

从图中我们可以看到“装饰”的过程:

《Head First 设计模式》模式3——装饰者模式为Starbuzz开发订单系统以装饰者构造饮料订单重构后的订单系统实现装饰者模式装饰者模式应用者——Java I/O

值得注意的是,调料

Mocha

Whip

也是

Beverage

类型,因为他们要装饰的对象是

Beverage

的子类,而它们本身亦可以被其他调料装饰。

由此构造我们可以知道:

  • 装饰者和被装饰对象具有相同的超类型
  • 可以用一个或多个对象包装一个对象
  • 装饰者可以在所委托被装饰者的行为前后加上自己的行为,达到特定目的
  • 对象可以在任何时候被装饰,所以可以在运行动态地用各种装饰者来装饰它

看来这个想法可行,我们尝试设计一下装饰者和被装饰者之间的关系类图:

《Head First 设计模式》模式3——装饰者模式为Starbuzz开发订单系统以装饰者构造饮料订单重构后的订单系统实现装饰者模式装饰者模式应用者——Java I/O

此处,

Decorator

(装饰者)里有一个

Component

(被装饰者)对象,用来保存对某个

Component

的引用。这样其子类(各种具体装饰者)就可以装饰任何

Component

对象了~

重构后的订单系统实现

好了,我们将上面新的设计引进到Starbuzz的订单系统中,重新设计饮料和调料的类结构:

《Head First 设计模式》模式3——装饰者模式为Starbuzz开发订单系统以装饰者构造饮料订单重构后的订单系统实现装饰者模式装饰者模式应用者——Java I/O

可能有人会有疑问:在策略模式和观察者模式里讲到,不是要多用组合少用继承吗,为什么这里是通过继承来动态地改变功能呢?

其实,装饰者和被装饰者必须是一样的类型,因为装饰者在装饰对象时它本身亦可以被其它装饰者装饰。所以,我们利用继承是想达到“类型匹配”,而不是通过继承获得“行为”。这点很重要!

如此一来,当装饰者和被装饰者组合时,就相当于动态地添加行为。这样得到的新行为,不是继承超类而是组合对象得到的。

下来通过代码来体验一下订单系统的类新结构:

首先建立基类

Beverage

// 饮料基类
public abstract class Beverage {
    protected String descrption;

    public String getDescription(){
        return descrption;
    }

    public abstract double cost();
}
           

再创建调料的抽象类

CondimentDecorator

// 调料基类
public abstract class CondimentDecorator extends Beverage {
    public abstract String getDescription();
}
           

饮料和调料基础类都有了,下面开始实现一些饮料:

// 超优深焙咖啡豆
public class DarkRoast extends Beverage {   
    DarkRoast() {
        descrption = "DarkRoast";
    }

    public double cost() {
        return ;
    }
}
           
// 浓缩咖啡
public class Espresso extends Beverage {    
    Espresso() {
        descrption = "Espresso";
    }

    public double cost() {
        return ;
    }
}
           
// 低咖啡因咖啡
public class Decaf extends Beverage {   
    Decaf() {
        descrption = "Decaf";
    }

    public double cost() {
        return ;
    }
}
           

再实现一些调料:

// 摩卡调料
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  + beverage.cost();
    }
}
           
// 豆浆调料
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  + beverage.cost();
    }
}
           
// 奶泡调料
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  + beverage.cost();
    }
}
           
// 牛奶调料
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  + beverage.cost();
    }
}
           

最后来测试一下新的订单系统是否计算正确:

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 Decaf();
        beverage3 = new Soy(beverage3);
        beverage3 = new Mocha(beverage3);
        beverage3 = new Milk(beverage3);
        System.out.println(beverage3.getDescription() + " $" + beverage3.cost());
    }
}
           

控制台的打印结果是:

Espresso $0.
DarkRoast, Mocha, Mocha, Whip $2.
Decaf, Soy, Mocha, Milk $1.
           

不仅计算正确,而且这全新设计的订单系统设计也符合Starbuzz的需求!

当然,这里创建装饰者还不够简洁,待到后面接触“工厂模式”时会介绍更好的方式创建装饰者对象~

装饰者模式

吃完栗子可以讲正确的废话了!

装饰者模式的定义:

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

装饰者模式应用者——Java I/O

我们知道,java.io包里有很多类,其实很多都是装饰者。

比如:用

BuffereadInputStream

装饰

FileInputStream

,用

LineNumberInputStream

装饰

BuffereadInputStream

。。。

为了更熟悉装饰者模式,我们自己也来写一个装饰者——把输入流内的所有大写字符转换成小写字符。

import java.io.*;

// 自己的java I/O 装饰者:将大写字符转小写
public class LowerCaseInputStream extends FilterInputStream {

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

    public int read() throws IOException {
        int c = super.read();
        return (c == - ? 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;
    }
}
           

测试一把:

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()) >= ) {
                System.out.print((char)c);
            }

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

继续阅读