box

我同事大头,喜欢吃面。这天大头去一家面馆吃面,由于大头饭量大,面没吃完觉得不够,有点了两个卤蛋,一会儿再叫了一份青菜,最后时刻又加了一碗豆浆。面我们称为主食,必须要点,其他称为小吃,可以随意组合点单也可不点,假如面的价格是8元,卤蛋2元一个,青菜5元一份,豆浆3元一碗,现在要计算总共价格,如何设计?要求具备良好的扩展性和维护性。

1. 大头吃面第一版

既然面馆提供这么多好吃的东西,最容易的想到的就是在类中增加方法,初版设计类图如下:

decorator pattern1
Figure 1. 大头吃面第一版设计类图

这种方式,将点主食、小吃分别加到管理类中,然后计算总价,实现代码如下:

class NoodleRestaurant {
    private int totalPrice;

    void orderNoodles(int count) {
        System.out.println("点了" + count + "份面");
        totalPrice += 8 * count;
    }

    void addEggs(int count) {
        System.out.println("点了" + count + "份鸡蛋");
        totalPrice += 2 * count;
    }

    void addVegetables(int count) {
        System.out.println("点了" + count + "份青菜");
        totalPrice += 5 * count;
    }

    void addSoySauce(int count) {
        System.out.println("点了" + count + "份豆浆");
        totalPrice += 3 * count;
    }

    public int getTotalPrice() {
        return totalPrice;
    }
}

客户端调用也很简单:

NoodleRestaurant noodleRestaurant = new NoodleRestaurant();
noodleRestaurant.orderNoodles(1);
noodleRestaurant.addEggs(2);
noodleRestaurant.addVegetables(1);
noodleRestaurant.addSoySauce(1);
System.out.println("大头总共花费:" + noodleRestaurant.getTotalPrice() + "元");

这样的设计,虽然很简单容易想到,但是如果面馆现在添加了更多的小吃,怎么办呢?那么只能在NoodleRestaurant类中继续增加方法来实现。很明显,这样就违背开闭原则,添加新功能的时候,原有代码也需要修改,并且,如果小吃和主食越来越多,类将变得非常庞大和臃肿。

改进的方式就是使用装饰模式。

2. 装饰模式

2.1. 装饰模式简介

装饰模式(Decorator Pattern)的定义:在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。装饰模式属于一种结构型模式,它比继承更灵活。

装饰模式的基本类图如下:

decorator pattern2
Figure 2. 装饰模式类图

装饰模式分为以下几个角色:

  • Component: 具有特定功能的对象(构件),可以是抽象类或者接口,也称为被装饰者,需要对其添加职责

  • ConcreteComponent: 特定功能对象(Component)的具体实现

  • Decorator: 装饰抽象类,用来动态的给Component添加职责,扩展功能

  • ConcreteDecorator: 具体的装饰类实现

注意Decorator和Component的关系:

  1. 首先,Decorator继承或者实现了Component,这样就可以具备原Component对象的功能

  2. 其次,Decorator内部还聚合了Component,通常包含一个setComponent(Component)方法,也可以通过构建器聚合Component对象,这样做的目的:便于层层包装,被装饰过的类由于继承了Component,仍然可以聚合到Decorator从而被再次包装

  3. 第三,对于Component而言,它无需知道Decorator的存在,只要客户端(Decorator的使用者)知道Decorator并使用它扩展的功能即可

装饰模式的特点:

  • 比继承灵活,在不改变原有对象的情况下,动态的给一个对象扩展功能,即插即用

  • 通过使用不同的装饰类及这些装饰类的排列组合,可以实现不同效果

  • 完全遵守开闭原则

2.2. 装饰模式举例

举个简单的例子,装饰模式就好比商家给商品打包,商品有贵重、一般、廉价之分,有的商品一点磕碰都不行,比如手机、笔记本电脑等,它们都需要厚厚的包装才能避免损坏;而有的商品则不然,比如衣服,只要简单包装即可,因为他们不怕摔,没必要填充泡沫、充气袋等缓冲材料。

因此,商家会按照商品的贵重程度、耐压程序来决定采用什么材料打包,通常都是将商品外边添加一层层的打包用材料,比如泡沫、充气袋,然后再装入塑料袋或者纸盒。这样的模式其实就是装饰模式,商品就是Component,泡沫、充气袋等打包材料就是装饰器。快递打包设计类图如下:

decorator pattern3
Figure 3. 商品包装类图

我们抽象了一个物品的接口,它作为Component,下边有电脑、衣服两种商品作为ConcreteComponent,然后快递包装类作为Decorator,具体的打包材料作为ConcreteDecorator。再来看看示例代码:

商品的代码
// 物品接口
interface Item { (1)
    void display();
}

// 电脑
class Computer implements Item { (2)
    @Override
    public void display() {
        System.out.println("一台电脑");
    }
}

// 衣服
class Clothes implements Item { (2)
    @Override
    public void display() {
        System.out.println("一件衣服");
    }
}
1Component角色,抽象类或者接口,定义功能规范
2ConcreteComponent角色,具体功能实现
装饰器的代码
// 快递包装,装饰者
abstract class GoodsDecorator implements Item { (1)
    // 被装饰者
    protected Item item;

    public GoodsDecorator(Item item) { (2)
        this.item = item;
    }

    public void setItem(Item item) { (2)
        this.item = item;
    }

    @Override
    public void display() {
        this.item.display();
        this.wrap();
    }

    // 包装快递
    protected abstract void wrap(); (3)
}
1装饰器继承了Item(Component角色)
2装饰器聚合了Item(Component角色)
3定义扩展方法,由子类实现
具体装饰器代码
// 塑料袋
class PlasticBag extends GoodsDecorator {
    public PlasticBag(Item item) {
        super(item);
    }

    @Override
    protected void wrap() { (1)
        System.out.println("装入塑料袋");
    }
}

// 泡沫材料
class FoamMaterial extends GoodsDecorator {
    public FoamMaterial(Item item) {
        super(item);
    }

    @Override
    protected void wrap() { (1)
        System.out.println("填充泡沫材料");
    }
}

// 纸板箱
class Carton extends GoodsDecorator {
    public Carton(Item item) {
        super(item);
    }

    @Override
    protected void wrap() { (1)
        System.out.println("装入纸箱");
    }
}

// 充气袋
class DunnageBag extends GoodsDecorator {
    public DunnageBag(Item item) {
        super(item);
    }

    @Override
    protected void wrap() { (1)
        System.out.println("填充充气袋");
    }
}
1子类(ConcreteDecorator角色)各自实现抽象装饰器定义的扩展方法

客户端调用也很简单:

客户端调用代码
System.out.println("商家打包一件衣服,不值钱,简单包装了事...");
Clothes clothes = new Clothes();
// 直接放入塑料袋就可以了
PlasticBag plasticBag = new PlasticBag(clothes);
plasticBag.display();

System.out.println("商家打包一台电脑,有点贵,还是做个良心商家好好包装一番...");
Computer computer = new Computer();
// 先用充气袋填充
DunnageBag dunnageBag = new DunnageBag(computer);
// 再用泡沫材料填充
FoamMaterial foamMaterial = new FoamMaterial(dunnageBag);
// 最后在装入纸箱,完成
Carton carton = new Carton(foamMaterial);
carton.display();

注意,客户端使用的是装饰器的display方法,该方法才得到了扩展,而不是使用Component角色的方法。

结果输出如下:

商家打包一件衣服,不值钱,简单包装了事...
一件衣服
装入塑料袋
商家打包一台电脑,有点贵,还是做个良心商家好好包装一番...
一台电脑
填充充气袋
填充泡沫材料
装入纸箱

Ok,了解了装饰模式,我们看看如何改进文章开始提到"大头吃面"问题。

3. 大头吃面第二版

面馆主要是供应的是主食,比如面条、饺子等,而小吃作为附加食品给客人提供了更多的选择。使用装饰模式,可以将小吃看做对主食的一种包装,改进后的类图如下:

decorator pattern4
Figure 4. 大头吃面第二版类图

类图中,抽象出了一个接口:Food,并且定义了cost()方法来计算价格,实现类Noodle支持设置点的面条的份数(count)。FoodDecorator作为装饰器,继承并聚合了Food,其子类都支持设置点单份数(count)。

改进后的代码如下:

Component角色代码
// 抽象构件:食物
interface Food {
    // 计算食物的价格
    int cost(); (1)
}

// 主食:面条
class Noodle implements Food {
    private int count = 1;

    public Noodle() {
        System.out.println("点了1份面");
    }

    public Noodle(int count) {
        System.out.println("点了" + count + "份面");
        this.count = count;
    }

    // 价格
    @Override
    public int cost() { (2)
        return 8 * this.count;
    }
}
1定义功能方法,计算价格
2功能方法的具体实现
Decorator角色代码
abstract class FoodDecorator implements Food {
    protected Food food;

    public FoodDecorator(Food food) {
        this.food = food;
    }

    @Override
    public int cost() { (1)
        return food.cost();
    }
}

// 卤蛋
class Egg extends FoodDecorator {
    private int count = 1;

    public Egg(Food food) {
        super(food);
        System.out.println("点了" + count + "份鸡蛋");
    }

    public Egg(Food food, int count) {
        super(food);
        System.out.println("点了" + count + "份鸡蛋");
        this.count = count;
    }

    @Override
    public int cost() { (2)
        return super.cost() + 2 * this.count;
    }
}

// 青菜
class Vegetable extends FoodDecorator {
    private int count = 1;

    public Vegetable(Food food) {
        super(food);
        System.out.println("点了" + count + "份青菜");
    }

    public Vegetable(Food food, int count) {
        super(food);
        System.out.println("点了" + count + "份青菜");
        this.count = count;
    }

    @Override
    public int cost() { (2)
        return super.cost() + 5 * this.count;
    }
}

// 豆浆
class SoySauce extends FoodDecorator {
    private int count = 1;

    public SoySauce(Food food) {
        super(food);
        System.out.println("点了" + count + "份豆浆");
    }

    public SoySauce(Food food, int count) {
        super(food);
        System.out.println("点了" + count + "份豆浆");
        this.count = count;
    }

    @Override
    public int cost() { (2)
        return super.cost() + 3 * count;
    }
}
1这里装饰器没有定义新的方式,而是重写了cost方法来计算总价
2子类重写父类的cost方法,实现商品总价的计算

客户端调用:

客户端代码
System.out.println("第二版:");
// 点了一碗面
Noodle noodle = new Noodle();
// 加两个鸡蛋
Egg egg = new Egg(noodle, 2);
// 加一份青菜
Vegetable vegetable = new Vegetable(egg);
// 加一份豆浆
SoySauce soySauce = new SoySauce(vegetable);
// 计算价格
int cost = soySauce.cost();
System.out.println("大头总共花费:" + cost + "元");

程序输出结果如下:

第二版:
点了1份面
点了2份鸡蛋
点了1份青菜
点了1份豆浆
大头总共花费:20元

如果现在面馆要新增加小吃或者主食,只需要添加新的类实现Food接口,或者继承FoodDecorator接口供客户端调用即可,原来设计的代码不需要做任何改动,符合开闭原则。

4. 装饰模式在JDK中的应用

装饰模式在JDK中使用比较广泛,尤其是在IO操作中,比如BufferedReaderDataInputStream等等,看一个例子:

BufferedReader reader = new BufferedReader(new FileReader(DecoratorInJdkDemo.class.getResource("test").getFile()));
String line;
while ((line = reader.readLine()) != null) {
    System.out.println(line);
}

示例展示用了从test文件逐行读取文本信息,这是这个BufferedReader就使用了装饰模式。

类的继承关系如下:

decorator pattern5
  • BufferedReader 从字符流中读取字符,并且提供缓冲区读取部分数据到内存处理,提高读取效率

  • InputStreamReader 用来读取字节流

  • FileReader 用于读取字符文件中的内容,它继承自InputStreamReader

BufferedReader类原码节选
public class BufferedReader extends Reader {
    private Reader in;

    // ......

    public BufferedReader(Reader in) {
        this(in, defaultCharBufferSize);
    }

    // ......
}

Reader是一个抽象类, 定义read抽象方法,BufferedReader继承Reader,并且聚合了Reader,这里的BufferedReader既作为抽象装饰器又作为装饰器实现,扩展了Reader的功能,添加了缓冲功能,而FileReader作为Reader的具体实现,详情可以参考JDK源代码。


相关阅读