跳转至

装饰者模式

分类:结构型模式。来源:《Head First 设计模式》第 3 章。

类结构

classDiagram
    direction TB
    class Component {
        <<interface>>
        +operation()
    }
    class ConcreteComponent {
        +operation()
    }
    class Decorator {
        <<abstract>>
        +operation()
    }
    class ConcreteDecorator {
        +operation()
        +addedBehavior()
    }
    Component <|.. ConcreteComponent
    Component <|.. Decorator
    Decorator <|-- ConcreteDecorator
    Decorator --> Component : 包装

角色定义

角色 定义
Component(组件接口) 定义被装饰对象与装饰者共同遵守的操作契约。
ConcreteComponent(具体组件) 实现组件接口,提供需要动态扩展的基础行为。
Decorator(抽象装饰者) 实现组件接口并持有另一个组件,将操作委托给被包装对象;该对象也可以是另一个装饰者。
ConcreteDecorator(具体装饰者) 继承抽象装饰者,在委托操作之前或之后添加职责,同时保持组件接口。

原书笔记

开放-关闭原则

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

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

认识装饰者模式

以饮料为主体,然后在运行时以调料来“装饰”饮料。比方说,如果顾客想要摩卡和奶泡深焙咖啡,那么,要做的是:

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

目前所知的一切:

  1. 装饰者和被装饰对象有相同的超类型,我们利用继承达到“类型匹配”,而不是利用继承获得“行为”,因为装饰者必须能取代被装饰者。
  2. 你可以用一个或多个装饰者包装一个对象
  3. 既然装饰者和被装饰对象有相同的超类型,所以在任何需要原始对象的场合,可以用装饰过的对象代替它。
  4. 装饰者可以在所委托被装饰者的行为之前或之后,加上自己的行为,以达到特定的目的。
  5. 对象可以在任何时候被装饰,所以可以在运行时动态地,不限量地用你喜欢的装饰者来装饰对象。

定义装饰者模式

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

写下星巴兹的代码

1
2
3
4
5
6
7
public abstract class Beverage {
    String description = "Unknown Beverage";
    public String getDescription(){
        return description;
    }
    public abstract double cost();
}

实现Condiment(调料)抽象类,也就是装饰者类吧:

1
2
3
public abstract class CondimentDecorator extends Beverage {
    public abstract String getDescription();
}

饮料的代码

//浓缩
public class Espresso extends Beverage {
    public Espresso(){
        description = "Espresso";
    }

    @Override
    public double cost() {
        return 1.99;
    }
}
//综合
public class HouseBlend extends Beverage {
    public HouseBlend(){
        description = "House Blend Coffee";
    }

    @Override
    public double cost() {
        return .89;
    }
}
//深焙咖啡
public class DarkRoast extends Beverage {
    public DarkRoast(){
        description = "Dark Roast Coffee";
    }
    public double cost() {
        return .99;
    }
}
//低咖啡因
public class Decaf extends Beverage {
    public Decaf(){
        description = "Decaf Coffee";
    }
    public double cost() {
        return 1.05;
    }
}
调料代码

//摩卡
public class Mocha extends CondimentDecorator {
    Beverage beverage;
    public Mocha(Beverage beverage){
        this.beverage = beverage;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription()+",Mocha";
    }
    public double cost(){
        return .20+beverage.cost();
    }
}
//牛奶
public class Milk extends Beverage{
    Beverage beverage;
    public Milk(Beverage beverage){
        this.beverage = beverage;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription()+",Milk";
    }
    public double cost(){
        return .10+beverage.cost();
    }
}
//豆浆
public class Soy extends Beverage {
    Beverage beverage;
    public Soy(Beverage beverage){
        this.beverage = beverage;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription()+",Soy";
    }
    public double cost(){
        return .15+beverage.cost();
    }
}
//奶泡
public class Whip extends Beverage {
    Beverage beverage;
    public Whip(Beverage beverage){
        this.beverage = beverage;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription()+",Whip";
    }
    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());
        Beverage beverage3 = new HouseBlend();
        beverage3 = new Soy(beverage3);
        beverage3 = new Mocha(beverage3);
        beverage3 = new Whip(beverage3);
        System.out.println(beverage3.getDescription() + " $" + beverage3.cost());
    }
}

输出结果

1
2
3
Espresso $1.99
Dark Roast Coffee,Mocha,Mocha,Whip
House Blend Coffee,Soy,Mocha,Whip$1.34

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

装饰java.io类

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

public class LowCaseInputStream extends FilterInputStream {
    public LowCaseInputStream(InputStream in) {
        super(in);
    }

    @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] = Character.toLowerCase((char) b[i]);
        }
        return result;
    }
}

测试你的新Java I/O装饰者

public class InputTest {
    public static void main(String[] args) {
        int c;
        try {
            InputStream in = new LowCaseInputStream(new BufferedInputStream(new FileInputStream("test.txt")));//test.txt内容 I know the Decorator Pattern therefore I RULE!
            while ((c = in.read()) >= 0) {
                System.out.print((char) c);
            }
            in.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
输出结果

i know the decorator pattern therefore i rule!

Head First 示例:星巴兹的双份摩卡奶泡咖啡

《Head First 设计模式》第 3 章用星巴兹点单说明装饰者:同一杯深焙咖啡可以逐层加入两份摩卡和一份奶泡,每层都保持饮料类型,并在基础价格上追加自己的费用。这里选取作者 2020 年第二版配套代码 decorator/starbuzz 中 StarbuzzCoffee 的第二杯饮料,不展开其他饮料和调料组合。

本节按该版本源码核对:Beverage 是抽象类;CondimentDecorator 继承它并声明被包装对象字段 beverage;Milk、Soy、Whip 都继承 CondimentDecorator。这些细节与上方保留的历史代码片段有所不同,下面的示例图以本节链接的固定版本为准。

具体类名 对应角色 职责
Beverage 抽象组件 提供 getDescription(),声明抽象方法 cost(),统一饮料与调料的使用方式。
DarkRoast 具体组件 设置深焙咖啡描述,返回基础价格 0.99。
CondimentDecorator 抽象装饰者 持有 Beverage beverage,要求子类实现 getDescription()。
Mocha 具体装饰者 委托给被包装饮料,再追加摩卡描述和 0.20 的费用。
Whip 具体装饰者 委托给被包装饮料,再追加奶泡描述和 0.10 的费用。
StarbuzzCoffee 客户端与示例入口 逐层包装饮料,通过 Beverage 读取最终描述与价格。

示例 UML

classDiagram
    direction TB
    class Beverage {
        <<abstract>>
        String description
        +getDescription() String
        +cost() double*
    }
    class DarkRoast {
        +cost() double
    }
    class CondimentDecorator {
        <<abstract>>
        Beverage beverage
        +getDescription() String*
    }
    class Mocha {
        +Mocha(Beverage beverage)
        +getDescription() String
        +cost() double
    }
    class Whip {
        +Whip(Beverage beverage)
        +getDescription() String
        +cost() double
    }
    class StarbuzzCoffee {
        +main(String[] args)$
    }
    Beverage <|-- DarkRoast
    Beverage <|-- CondimentDecorator
    CondimentDecorator <|-- Mocha
    CondimentDecorator <|-- Whip
    CondimentDecorator --> Beverage : beverage 包装
    StarbuzzCoffee ..> Beverage : 读取描述与价格
    StarbuzzCoffee ..> DarkRoast : 创建基础饮料
    StarbuzzCoffee ..> Mocha : 包装两次
    StarbuzzCoffee ..> Whip : 包装最外层

图中只保留双份摩卡奶泡深焙咖啡所需类型,省略其他饮料、调料和带杯型的另一个示例包。继承使调料拥有饮料类型,beverage 关联使其能够在运行时包住另一个饮料或装饰者。

协作过程

  1. StarbuzzCoffee 创建 DarkRoast,用 Beverage 变量保存它。
  2. 连续两次创建 Mocha,每次把当前饮料传入构造器,再将变量指向新的包装层;最后用 Whip 包装最外层。
  3. 调用最外层 getDescription(),各层先取得内层描述再追加自己的调料名称,因此两份摩卡都会出现在结果中。
  4. 调用最外层 cost(),费用沿包装链累计为 0.99 + 0.20 + 0.20 + 0.10 = 1.49;入口打印最终描述和价格。

固定版本源码:

评论