《Head First 设计模式》模式3——装饰者模式

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

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


为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 = 0;
        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 1.99 + super.cost();
    }
}

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

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

以装饰者构造饮料订单

从上面的改造可以看到,当新功能出现时(添加新的调料)时,就要修改Beverage的代码,不能动态地组合对象以避免修改。这是不符合“开闭原则”的!

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

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

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

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

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

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

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

值得注意的是,调料MochaWhip也是Beverage类型,因为他们要装饰的对象是Beverage的子类,而它们本身亦可以被其他调料装饰。

由此构造我们可以知道:

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

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

此处,Decorator(装饰者)里有一个Component(被装饰者)对象,用来保存对某个Component的引用。这样其子类(各种具体装饰者)就可以装饰任何Component对象了~

重构后的订单系统实现

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

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

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

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

下来通过代码来体验一下订单系统的类新结构:
首先建立基类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 1.99;
    }
}
// 浓缩咖啡
public class Espresso extends Beverage {    
    Espresso() {
        descrption = "Espresso";
    }

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

    public double cost() {
        return 1.05;
    }
}

再实现一些调料:

// 摩卡调料
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();
    }
}
// 豆浆调料
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();
    }
}
// 奶泡调料
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 .15 + 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 .10 + 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.89
DarkRoast, Mocha, Mocha, Whip $2.54
Decaf, Soy, Mocha, Milk $1.5

不仅计算正确,而且这全新设计的订单系统设计也符合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 == -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;
    }
}

测试一把:

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();
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值