本篇我们将介绍一个有意思的设计模式,装饰器模式。如果你想快速记忆装饰器模式,那么只需要抓住一个特征:装饰器模式可以像俄罗斯套娃一样,将对象层层嵌套。
一、问题引入和解决方案
1.1 继承带来类爆炸
我家附件有很多早餐店:餐馆里有面条(8元/份)、米线(7元/份)、抄手(6元/份)、混沌(10元/份)等主食,有牛肉(3元/份)、海鲜(5元/份)、羊肉(5元/份)、酸菜(2元/份)等辅料。现在有客人进入餐馆,任意点餐,包含有(一种)主食和(一种或多种)辅料,如何计算出这份早餐花费的价格和描述这份早餐?
我们可以定义一个接口(接口中包含两个行为:计算价格,描述早餐),然后列举出每一种早餐,就像下面这样:
我们很快就意识到一些问题:
(一)当前我们仅有 4 种主食,4 种辅料,那么任意一种主食和辅料的搭配组合就有 16 种了,如果某一天店里新开了一种新的主食或辅料,我们不得不为其新增多个类表示; (二)顾客在点餐时,很有可能需要多种辅料,比如客户点了一份酸菜牛肉米线。此时,我们不得不考虑同一种主食搭配多种辅料的情况了; (三)此时,进来一位客人:老板,我要一份羊肉酸菜面,我喜欢吃酸,酸菜要双倍!!!
正如上面这样,我们终于发现这个思路并不能解决实际问题。我们在处理主食和辅料的搭配时就已经引入了太多的类表示,更别提一种主食搭配多种辅料的情况,现在还不得不考虑客人在点餐时期望辅料加倍的需求。一旦我们增加一种主食或辅料,系统中类的数量将呈现几何式增长,这就是通俗说的类爆炸。那么我们该如何解决这个问题呢?
1.2 解决方案
让我们回想一下,厨师是如何制作一碗双倍酸菜的牛肉面条的呢?第一步,面条煮熟捞入碗中;第二步,加入一份牛肉;第三步,加入一份酸菜;第四步,再加入一份酸菜。让我们仔细分析这个过程:
- 在面条煮熟捞入碗中时,这已经就是一份早餐了,价格就是面条的价格;
- 厨师往碗中加入一份牛肉,它仍然是一份早餐,区别在于这份早餐中加了一份牛肉,而价格在原来的基础上增加了一份牛肉的价格;
- 厨师再往碗中加入一份酸菜,还是一份早餐,仅是在原来的早餐中又加了一份酸菜,而价格也是在原来的基础上增加了一份酸菜的价格;
- 以此往复。
对于一份早餐来说,每一次往其中加入辅料,变化的价格是确定的:此此加入辅料的价格。不管加何种辅料,加了多少次辅料,其本质都还是早餐。所以,如果将一份早餐看成一个对象 A,往早餐中放入一份辅料后的早餐是对象 B,那么对象 B 可以看成是在对象 A 的基础之上包装了一份辅料,所以价格就是 A 的价格和 B 中包含辅料的价格之和。
所以,如果我们想要计算一份早餐的价格,我们需要知道的是:每一个上一次往碗中加入了什么辅料。这是一个不断递归的过程,递归求解的终点是:上一次往碗中加入的是主食。
根据上面的分析,我们不难总结出以下几个要点:
- 从碗中加入主食开始,每一次加入辅料都没有改变这是一份早餐的本质;
- 食材分为两种:主食和辅料,一份早餐中主食有且仅有一份,辅料可无限叠加;
- 对于一份早餐的价格,计算方式就是不断回溯,直到最原始的主食,再根据每一次加入的辅料,价格叠加。
1.3 求解过程
针对上面总结的分析,我们对一份早餐价格的计算过程就像下图所示:
二、实现解决方案
2.1 类图结构
根据解决方案中总结的要点,我们设计出如下的类图结构:
在前面我们提到,每一次加入辅料都是在新对象中包装了原来的对象,所以在辅料类(BreakfastDecorator)中维护一个对于原来早餐对象的引用(breakfast)。例如 o3 表示在 o2 基础上加入一份辅料后的早餐,那么 o3 价格 = o3 所表示辅料的价格 + o2 的价格,以此类推。不管如何往碗中加入辅料,都是早餐,所以所有的对象继承自 Breakfast 类。主食总是第一个加入碗中,主食代表着递归的终点,所以主食没有对于维护另一个早餐对象的引用。这就是装饰器模式。
2.2 实现代码
为了简化代码,我们主食只考虑面条和粉条,辅料只包含牛肉、羊肉和酸菜。
2.2.1 早餐定义
public interface Breakfast {
/**
* 描述
* @return 早餐的内容
*/
String getDescription();
/**
* 计算费用
* @return 早餐的价格
*/
int cost();
}
2.2.2 主食
public class Noodles implements Breakfast {
@Override
public String getDescription() {
return "面条";
}
@Override
public int cost() {
return 8;
}
}
public class Vermicelli implements Breakfast {
@Override
public String getDescription() {
return "粉条";
}
@Override
public int cost() {
return 7;
}
}
2.2.3 辅料抽象
public abstract class BreakfastDecorator implements Breakfast {
/**
* 包装的具体组件
*/
protected Breakfast breakfast;
public BreakfastDecorator(Breakfast breakfast) {
this.breakfast = breakfast;
}
}
2.2.4 辅料
public class BeefDecorator extends BreakfastDecorator {
public BeefDecorator(Breakfast breakfast) {
super(breakfast);
}
@Override
public String getDescription() {
return this.breakfast.getDescription() + " + 牛肉";
}
@Override
public int cost() {
return this.breakfast.cost() + 3;
}
}
public class MuttonDecorator extends BreakfastDecorator {
public MuttonDecorator(Breakfast breakfast) {
super(breakfast);
}
@Override
public String getDescription() {
return this.breakfast.getDescription() + " + 羊肉";
}
@Override
public int cost() {
return this.breakfast.cost() + 5;
}
}
public class SauerkrautDecorator extends BreakfastDecorator {
public SauerkrautDecorator(Breakfast breakfast) {
super(breakfast);
}
@Override
public String getDescription() {
return this.breakfast.getDescription() + " + 酸菜";
}
@Override
public int cost() {
return this.breakfast.cost() + 2;
}
}
2.2.5 客户端
public class Client {
public static void main(String[] args) {
System.out.println("|==> Start ---------------------------------------------|");
System.out.println(" Tom 点了一份牛肉面");
Breakfast breakfast4Tom = new BeefDecorator(new Noodles());
System.out.println(" 花费:" + breakfast4Tom.cost());
System.out.println(" 食材包含有:" + breakfast4Tom.getDescription());
System.out.println(" Jack 点了一份酸菜羊肉米粉,酸菜要了双份");
Breakfast breakfast4Jack = new SauerkrautDecorator(new SauerkrautDecorator(new MuttonDecorator(new Vermicelli())));
System.out.println(" 花费:" + breakfast4Jack.cost());
System.out.println(" 食材包含有:" + breakfast4Jack.getDescription());
}
}
|==> Start ---------------------------------------------|
Tom 点了一份牛肉面
花费:11
食材包含有:面条 + 牛肉
Jack 点了一份酸菜羊肉米粉,酸菜要了双份
花费:16
食材包含有:粉条 + 羊肉 + 酸菜 + 酸菜
三、装饰器模式
3.1 类图结构
让我们看一下更加通用的装饰器模式的类图结构:
装饰器模式的参与角色如下:
- Component:组件,定义一个对象接口。类比例子中的早餐接口,不管是主食还是辅料,都是早餐;
- ConcreteComponent:具体的组件。类比例子中的面条;
- Decorator:抽象的装饰器,本身是组件的实现,同时也包装了一个组件。类比例子中的辅料抽象类,本身是早餐,也装饰了另一份早餐;
- ConcreteDecorator:具体的装饰器实现。类比例子中的酸菜、牛肉等辅料;
3.2 意图
指在不改变原有对象结构的基础情况下,动态地给该对象增加一些额外功能的职责。
装饰器模式的核心就是在运行时,不停的给一个对象添加一些功能,操作方式将一个包装好的对象作为原始对象再次进行包装。
想象一下,我们煮了一碗面条,往面条里面放入一勺牛肉,这样就变成了牛肉面;再往面条里面放入一勺酸菜,这样就变成了酸菜牛肉面。这就很像中文里面的形容词和名词的关系,一个名词可以被多个形容词进行修饰。
四、在源码中看装饰器模式
我们在各种源码中就能看到装饰器模式的影子。
(1)例如,java.io包下面的 InputStream 类图大致如下(篇幅原因,只放了部分类上去)
(2)再比如,在 javax.servelet 包下,ServletRequestWrapper 是 ServletRequest 接口的装饰器实现,开发者可以继承 ServletRequestWrapper 去扩展原来的 ServletRequest。
附录
案例代码:…/decorator