摘要
JDK:1.8.0_202
# 一:场景问题
# 1.1 奖金计算
考虑这样一个实际应用:就是如何实现灵活的奖金计算。
奖金计算是相对复杂的功能,尤其是对于业务部门的奖金计算方式,是非常复杂的,除了业务功能复杂外,另外一个麻烦之处是计算方式还经常需要变动,因为业务部门经常通过调整奖金的计算方式来激励士气。
先从业务上看看现有的奖金计算方式的复杂性:
首先是奖金分类,对于个人,大致有个人当月业务奖金、个人累计奖金、个人业务增长奖金、及时回款奖金、限时成交加码奖金等等;
对于业务主管或者是业务经理,除了个人奖金外,还有:团队累计奖金、团队业务增长奖金、团队盈利奖金等等。
其次是计算奖金的金额,又有这么几个基数:销售额、销售毛利、实际回款、业务成本、奖金基数等等;
另外一个就是计算的公式,针对不同的人、不同的奖金类别、不同的计算奖金的金额,计算的公式是不同的,就算是同一个公式,里面计算的比例参数也有可能是不同的。
# 1.2 简化奖金计算
为了后面演示的需要,简化一下演示用的奖金计算体系如下:
每个人当月业务奖金 = 当月销售额 X 3%
每个人累计奖金 = 总的回款额 X 0.1%
团队奖金 = 团队总销售额 X 1%
# 1.3 不用模式的解决方案
一个人的奖金分成很多个部分,要实现奖金计算,主要就是要按照各个奖金计算的规则,把这个人可以获取的每部分奖金计算出来,然后计算一个总和,这就是这个人可以得到的奖金
测试数据,用于模拟数据库数据,示例代码如下:
/**
* 在内存中模拟数据库,准备点测试数据,好计算奖金
*/
public class TempDB {
/**
* 记录每个人的月度销售额,只用了人员,月份没有用
*/
public static Map<String, BigDecimal> mapMonthSaleMoney = new HashMap<>();
static {
// 填充测试数据
mapMonthSaleMoney.put("张三", new BigDecimal(10000));
mapMonthSaleMoney.put("李四", new BigDecimal(20000));
mapMonthSaleMoney.put("王五", new BigDecimal(30000));
}
private TempDB() {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
实现奖金计算,示例代码如下:
/**
* 计算奖金的对象
*/
public class Prize {
/**
* 计算某人所得奖金
*
* @param user 被计算奖金的人员
* @return 某人在某段时间内的奖金
*/
public BigDecimal calcPrize(String user) {
BigDecimal prize;
// 计算业务奖金
prize = this.monthPrize(user);
// 计算累计奖金
prize = prize.add(this.sumPrize(user));
// 需要判断该人员是普通人员还是业务经理,团队奖金只有业务经理才有
if (this.isManager(user)) {
prize = prize.add(this.groupPrize(user));
}
return prize;
}
/**
* 计算某人当月业务奖金
*/
private BigDecimal monthPrize(String user) {
// 计算当月业务奖金,按照人员去获取当月的业务额,然后再乘3%
BigDecimal prize = TempDB.mapMonthSaleMoney.get(user).multiply(new BigDecimal("0.3"));
System.out.println(user + "当月业务奖金" + prize.doubleValue());
return prize;
}
/**
* 计算某人的累计奖金
*/
private BigDecimal sumPrize(String user) {
// 计算累计奖金,其实应该按照人员去获取累计的业务额,然后再乘以0.1%
// 简单演示一下,假定大家的累计业务额都是1000000元
BigDecimal prize = new BigDecimal(1000000).multiply(new BigDecimal("0.001"));
System.out.println(user + "累计奖金" + prize.doubleValue());
return prize;
}
/**
* 判断人员是普通还是业务经理
*
* @param user 被判断的人员
* @return true表示业务经理,false表示普通人员
*/
private boolean isManager(String user) {
// 演示,简单判断,只有王五为经理
return "王五".equals(user);
}
/**
* 计算当月团队业务奖
*
* @param user
* @return
*/
private BigDecimal groupPrize(String user) {
// 假设都是一个团队
BigDecimal group = BigDecimal.ZERO;
for (BigDecimal bg : TempDB.mapMonthSaleMoney.values()) {
group = group.add(bg);
}
BigDecimal prize = group.multiply(new BigDecimal("0.01"));
System.out.println(user + "当月团队业务奖金" + prize.doubleValue());
return prize;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
客户端:
public class Client {
public static void main(String[] args) {
// 先创建计算奖金的对象
Prize p = new Prize();
BigDecimal zs = p.calcPrize("张三");
System.out.println("==========张三应得奖金:" + zs.doubleValue());
BigDecimal ls = p.calcPrize("李四");
System.out.println("==========李四应得奖金:" + ls.doubleValue());
BigDecimal ww = p.calcPrize("王五");
System.out.println("==========王经理应得奖金:" + ww.doubleValue());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
运行结果:
# 1.4 有何问题
看了上面的实现,挺简单的嘛,就是计算方式麻烦点,每个规则都要实现。真的很简单吗?仔细想想,有没有什么问题?
对于奖金计算,光是计算方式复杂,也就罢了,不过是实现起来会困难点,相对而言还是比较好解决的,不过是用程序把已有的算法表达出来。
最痛苦的是,这些奖金的计算方式,经常发生变动,几乎是每个季度都会有小调整,每年都有大调整,这就要求软件的实现要足够灵活,要能够很快进行相应调整和修改,否则就不能满足实际业务的需要。
举个简单的例子来说,现在根据业务需要,需要增加一个“环比增长奖金”,就是本月的销售额比上个月有增加,而且要达到一定的比例,当然增长比例越高,奖金比例越大。那么软件就必须要重新实现这么个功能,并正确的添加到系统中去。过了两个月,业务奖励的策略发生了变化,不再需要这个奖金了,或者是另外换了一个新的奖金方式了,那么软件就需要把这个功能从软件中去掉,然后再实现新的功能。
那么上面的要求该如何实现呢?
很明显,一种方案是通过继承来扩展功能;另外一种方案就是到计算奖金的对象里面,添加或者删除新的功能,并在计算奖金的时候,调用新的功能或是不调用某些去掉的功能,这种方案会严重违反开-闭原则。
还有一个问题,就是在运行期间,不同人员参与的奖金计算方式也是不同的,举例来说:如果是业务经理,除了参与个人计算部分外,还要参加团队奖金的计算,这就意味着需要在运行期间动态来组合需要计算的部分,也就是会有一堆的if-else。
总结一下,奖金计算面临如下问题:
- 计算逻辑复杂;
- 要有足够灵活性,可以方便的增加或者减少功能;
- 要能动态的组合计算方式,不同的人参与的计算不同;
上面描述的奖金计算的问题,绝对没有任何夸大成分,相反已经简化不少了,还有更多麻烦没有写上来,毕竟我们的重点在设计模式,而不是业务。
把上面的问题抽象一下,设若有一个计算奖金的对象,现在需要能够灵活的给它增加和减少功能,还需要能够动态的组合功能,每个功能就相当于在计算奖金的某个部分。
现在的问题就是:如何才能够透明的给一个对象增加功能,并实现功能的动态组合呢?
# 二:解决方案
用来解决上述问题的一个合理的解决方案,就是使用装饰模式。
装饰模式:动态地给一个对象添加一些额外的职责。就添加来说,装饰模式比成子类更加灵活
# 2.1 解决思路
虽然经过简化,业务简单了很多,但是需要解决的问题不会少,还是要解决:要透明的给一个对象增加功能,并实现功能的动态组合。
所谓透明的给一个对象增加功能,换句话说就是要给一个对象增加功能,但是不能让这个对象知道,也就是不能去改动这个对象。而实现了能够给一个对象透明的增加功能,自然就能够实现功能的动态组合,比如原来的对象有A功能,现在透明的给它增加了一个B功能,是不是就相当于动态组合了A和B功能呢。
要想实现透明的给一个对象增加功能,也就是要扩展对象的功能了,使用继承啊,有人马上提出了一个方案,但很快就被否决了,那要减少或者修改功能呢?事实上继承是非常不灵活的复用方式。那就用 "对象组合" 嘛,又有人提出新的方案来了,这个方案得到了大家的赞同。
在装饰模式的实现中,为了能够和原来使用被装饰对象的代码实现无缝结合,是通过定义一个抽象类,让这个类实现与被装饰对象相同的接口,然后在具体实现类里面,转调被装饰的对象,在转调的前后添加新的功能,这就实现了给被装饰对象增加功能,这个思路跟 "对象组合" 非常类似。
在转调的时候,如果觉得被装饰的对象的功能不再需要了,还可以直接替换掉,也就是不再转调,而是在装饰对象里面完全全新的实现。
# 2.2 模式结构和说明
装饰模式的结构如图所示:
- Component:组件对象的接口,可以给这些对象动态的添加职责。
- ConcreteComponent:具体的组件对象,实现组件对象接口,通常就是被装饰器装饰的原始对象,也就是可以给这个对象添加职责。
- Decorator:所有装饰器的抽象父类,需要定义一个与组件接口一致的接口,并持有一个Component对象,其实就是持有一个被装饰的对象。注意,这个被装饰的对象不一定是最原始的那个对象了,也可能是被其它装饰器装饰过后的对象,反正都是实现的同一个接口,也就是同一类型。
- ConcreteDecorator:实际的装饰器对象,实现具体要向被装饰对象添加的功能。
# 2.3 示例代码
组件对象的接口定义:
/**
* 组件对象的接口,可以给这些对象动态的添加职责
*/
public abstract class Component {
/**
* 示例方法
*/
public abstract void operation();
}
2
3
4
5
6
7
8
9
10
11
具体组件实现:
/**
* 具体实现组件对象接口的对象
*/
public class ConcreteComponent extends Component {
@Override
public void operation() {
// 相应的功能处理
}
}
2
3
4
5
6
7
8
9
10
11
抽象的装饰器对象:
/**
* 装饰器接口,维持一个指向组件对象的接口对象,并定义一个与组件接口一致的接口
*/
public abstract class Decorator extends Component {
/**
* 持有组件对象
*/
protected Component component;
/**
* 构造方法,传入组件对象
*
* @param component 组件对象
*/
public Decorator(Component component) {
this.component = component;
}
public void operation() {
// 转发请求给组件对象,可以在转发前后执行一些附加动作
component.operation();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
装饰器实现对象,示意添加状态:
/**
* 装饰器的具体实现对象,向组件对象添加职责
*/
public class ConcreteDecoratorA extends Decorator {
/**
* 添加的状态
*/
private String addedState;
public ConcreteDecoratorA(Component component) {
super(component);
}
public String getAddedState() {
return addedState;
}
public void setAddedState(String addedState) {
this.addedState = addedState;
}
@Override
public void operation() {
//调用父类的方法,可以在调用前后执行一些附加动作
//在这里进行处理的时候,可以使用添加的状态
super.operation();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
装饰器实现对象,示意添加职责:
/**
* 装饰器的具体实现对象,向组件对象添加职责
*/
public class ConcreteDecoratorB extends Decorator {
public ConcreteDecoratorB(Component component) {
super(component);
}
/**
* 需要添加的职责
*/
private void addedBehavior() {
//需要添加的职责实现
}
public void operation() {
//调用父类的方法,可以在调用前后执行一些附加动作
super.operation();
addedBehavior();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 2.4 重写案例
首先需要定义一个组件对象的接口,在这个接口里面定义计算奖金的业务方法,因为外部就是使用这个接口来操作装饰模式构成的对象结构中的对象
需要添加一个基本的实现组件接口的对象,可以让它返回奖金为0就可以了
把各个计算奖金的规则当作装饰器对象,需要为它们定义一个统一的抽象的装饰器对象,好约束各个具体的装饰器的接口
把各个计算奖金的规则实现成为具体的装饰器对象
计算奖金的组件接口:
/**
* 计算奖金的组件接口
*/
public abstract class Component {
/**
* 计算某人在某段时间内的奖金
*
* @param user 被计算奖金的人员
* @return 某人当月奖金
*/
public abstract BigDecimal calcPrize(String user);
}
2
3
4
5
6
7
8
9
10
11
12
13
基本的实现对象:
/**
* 基本的实现计算奖金的类,也是被装饰器装饰的对象
*/
public class ConcreteComponent extends Component {
@Override
public BigDecimal calcPrize(String user) {
// 只是一个默认的实现,默认没有奖金
return BigDecimal.ZERO;
}
}
2
3
4
5
6
7
8
9
10
11
定义抽象装饰器:
/**
* 装饰器的接口,需要跟被装饰的对象实现同样的接口
*/
public abstract class Decorator extends Component {
/**
* 持有被装饰的组件对象
*/
protected Component c;
/**
* 通过构造方法传入被装饰的对象
*
* @param c 被装饰的对象
*/
public Decorator(Component c) {
this.c = c;
}
@Override
public BigDecimal calcPrize(String user) {
// 转调组件对象的方法
return c.calcPrize(user);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
定义一系列装饰器对象:
/**
* 装饰器对象,计算当月业务奖金
*/
public class MonthPrizeDecorator extends Decorator {
public MonthPrizeDecorator(Component c) {
super(c);
}
@Override
public BigDecimal calcPrize(String user) {
// 1:先获取前面运算出来的奖金
BigDecimal money = super.calcPrize(user);
// 2:然后计算当月业务奖金,按人员和时间去获取当月业务额,然后再乘以3%
BigDecimal prize = TempDB.mapMonthSaleMoney.get(user).multiply(new BigDecimal("0.03"));
System.out.println(user + "当月业务奖金" + prize.doubleValue());
return money.add(prize);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 装饰器对象,计算累计奖金
*/
public class SumPrizeDecorator extends Decorator {
public SumPrizeDecorator(Component c) {
super(c);
}
public BigDecimal calcPrize(String user) {
// 1:先获取前面运算出来的奖金
BigDecimal money = super.calcPrize(user);
// 2:然后计算累计奖金,其实应按人员去获取累计的业务额,然后再乘以0.1%
// 简单演示一下,假定大家的累计业务额都是1000000元
BigDecimal prize = new BigDecimal(1000000).multiply(new BigDecimal("0.001"));
System.out.println(user + "累计奖金" + prize.doubleValue());
return money.add(prize);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 装饰器对象,计算当月团队业务奖金
*/
public class GroupPrizeDecorator extends Decorator {
public GroupPrizeDecorator(Component c) {
super(c);
}
public BigDecimal calcPrize(String user) {
// 1:先获取前面运算出来的奖金
BigDecimal money = super.calcPrize(user);
// 2:然后计算当月团队业务奖金,先计算出团队总的业务额,然后再乘以1%
// 假设都是一个团队的
BigDecimal group = BigDecimal.ZERO;
for (BigDecimal d : TempDB.mapMonthSaleMoney.values()) {
group = group.add(d);
}
BigDecimal prize = group.multiply(new BigDecimal("0.01"));
System.out.println(user + "当月团队业务奖金" + prize.doubleValue());
return money.add(prize);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
客户端:
使用装饰器的客户端,首先需要创建被装饰的对象,然后创建需要的装饰对象,接下来重要的工作就是组合装饰对象,依次对前面的对象进行装饰。
有很多类似的例子,比如生活中的装修,就拿装饰墙壁来说吧:没有装饰前是原始的砖墙,这就好比是被装饰的对象,首先需要刷腻子,把墙找平,这就好比对原始的砖墙进行了一次装饰,而刷的腻子就好比是一个装饰器对象;好了,装饰一回了,接下来该刷墙面漆了,这又好比装饰了一回,刷的墙面漆就好比是又一个装饰器对象,而且这回被装饰的对象不是原始的砖墙了,而是被腻子装饰器装饰过后的墙面,也就是说后面的装饰器是在前面的装饰器装饰过后的基础之上,继续装饰的,类似于一层一层叠加的功能。
同样的道理,计算奖金也是这样,先创建基本的奖金对象,然后组合需要计算的奖金类型,依次组合计算,最后的结果就是总的奖金。示例代码如下:
/**
* 使用装饰模式的客户端
*/
public class Client {
public static void main(String[] args) {
// 先创建计算基本奖金的类,这也是被装饰的对象
Component c1 = new ConcreteComponent();
// 然后对计算的基本奖金进行装饰,这里要组合各个装饰
// 说明,各个装饰者之间最好是不要有先后顺序的限制,
// 也就是先装饰谁和后装饰谁都应该是一样的
// 先组合普通业务人员的奖金计算
Decorator d1 = new MonthPrizeDecorator(c1);
Decorator d2 = new SumPrizeDecorator(d1);
// 注意:这里只需使用最后组合好的对象调用业务方法即可,会依次调用回去
BigDecimal zs = d2.calcPrize("张三");
System.out.println("==========张三应得奖金:" + zs.doubleValue());
BigDecimal ls = d2.calcPrize("李四");
System.out.println("==========李四应得奖金:" + ls.doubleValue());
// 如果是业务经理,还需要一个计算团队的奖金计算
Decorator d3 = new GroupPrizeDecorator(d2);
BigDecimal ww = d3.calcPrize("王五");
System.out.println("==========王经理应得奖金:" + ww.doubleValue());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
运行结果:
当测试运行的时候会按照装饰器的组合顺序,依次调用相应的装饰器来执行业务功能,是一个递归的调用方法,以业务经理“王五”的奖金计算做例子,画个图来说明奖金的计算过程吧,看看是如何调用的
这个图很好的揭示了装饰模式的组合和调用过程,请仔细体会一下。
如同上面的示例,对于基本的计算奖金的对象而言,由于计算奖金的逻辑太过于复杂,而且需要在不同的情况下进行不同的运算,为了灵活性,把多种计算奖金的方式分散到不同的装饰器对象里面,采用动态组合的方式,来给基本的计算奖金的对象增添计算奖金的功能,每个装饰器相当于计算奖金的一个部分。
这种方式明显比为基本的计算奖金的对象增加子类来得更灵活,因为装饰模式的起源点是采用对象组合的方式,然后在组合的时候顺便增加些功能。为了达到一层一层组装的效果,装饰模式还要求装饰器要实现与被装饰对象相同的业务接口,这样才能以同一种方式依次组合下去。
灵活性还体现在动态上,如果是继承的方式,那么所有的类实例都有这个功能了,而采用装饰模式,可以动态的为某几个对象实例添加功能,而不是对整个类添加功能。比如上面示例中,客户端测试的时候,对张三李四就只是组合了两个功能,对王五就组合了三个功能,但是原始的计算奖金的类都是一样的,只是动态的为它增加的功能不同而已。
# 三:模式讲解
# 3.1 认识装饰模式
1. 模式功能
装饰模式能够实现动态的为对象添加功能,是从一个对象外部来给对象增加功能,相当于是改变了对象的外观。当装饰过后,从外部使用系统的角度看,就不再是使用原始的那个对象了,而是使用被一系列的装饰器装饰过后的对象。
这样就能够灵活的改变一个对象的功能,只要动态组合的装饰器发生了改变,那么最终所得到的对象的功能也就发生了改变。
变相的还得到了另外一个好处,那就是装饰器功能的复用,可以给一个对象多次增加同一个装饰器,也可以用同一个装饰器装饰不同的对象。
2. 对象组合
前面已经讲到了,一个类的功能的扩展方式,可以是继承,也可以是功能更强大、更灵活的对象组合的方式。
其实,现在在面向对象设计中,有一条很基本的规则就是“尽量使用对象组合,而不是对象继承”来扩展和复用功能。装饰模式的思考起点就是这个规则,可能有些朋友还不太熟悉什么是 "对象组合",下面介绍一下 "对象组合"。
什么是对象组合
直接举例来说吧,假若有一个对象A,实现了一个a1的方法,而C1对象想要来扩展A的功能,给它增加一个c11的方法,那么一个方案是继承,A对象示例代码如下:
public class A {
public void a1(){
System.out.println("now in A.a1");
}
}
2
3
4
5
C1对象示例代码如下:
public class C1 extends A{
public void c11(){
System.out.println("now in C1.c11");
}
}
2
3
4
5
另外一个方案就是使用对象组合,怎么组合呢?就是在C1对象里面不再继承A对象了,而是去组合使用A对象的实例,通过转调A对象的功能来实现A对象已有的功能,写个新的对象C2来示范,示例代码如下:
public class C2 {
/**
* 创建A对象的实例
*/
private A a = new A();
public void a1(){
//转调A对象的功能
a.a1();
}
public void c11(){
System.out.println("now in C2.c11");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
大家想想,在转调前后是不是还可以做些功能处理呢?对于A对象是不是透明的呢?对象组合是不是也很简单,而且更灵活了:
首先可以有选择的复用功能,不是所有A的功能都会被复用,在C2中少调用几个A定义的功能就可以了;
其次在转调前后,可以实现一些功能处理,而且对于A对象是透明的,也就是A对象并不知道在a1方法处理的时候被追加了功能;
还有一个额外的好处,就是可以组合拥有多个对象的功能,假如还有一个对象B,而C2也想拥有B对象的功能,那很简单,再增加一个方法,然后转调B对象就好了;
B对象示例如下:
public class B {
public void b1(){
System.out.println("now in B.b1");
}
}
2
3
4
5
同时拥有A对象功能,B对象的功能,还有自己实现的功能的C3对象示例代码如下:
public class C3 {
private A a = new A();
private B b = new B();
public void a1(){
//转调A对象的功能
a.a1();
}
public void b1(){
//转调B对象的功能
b.b1();
}
public void c11(){
System.out.println("now in C3.c11");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
最后再说一点,就是关于对象组合中,何时创建被组合对象的实例:
一种方案是在属性上直接定义并创建需要组合的对象实例;
另外一种方案是在属性上定义一个变量,来表示持有被组合对象的实例,具体实例从外部传入,也可以通过IoC/DI容器来注入;
public class C4 {
//示例直接在属性上创建需要组合的对象
private A a = new A();
//示例通过外部传入需要组合的对象
private B b = null;
public void setB(B b) {
this.b = b;
}
public void a1() {
//转调A对象的功能
a.a1();
}
public void b1() {
//转调B对象的功能
b.b1();
}
public void c11() {
System.out.println("now in C4.c11");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
3. 装饰器
装饰器实现了对被装饰对象的某些装饰功能,可以在装饰器里面调用被装饰对象的功能,获取相应的值,这其实是一种递归调用。
在装饰器里不仅仅是可以给被装饰对象增加功能,还可以根据需要选择是否调用被装饰对象的功能,如果不调用被装饰对象的功能,那就变成完全重新实现了,相当于动态修改了被装饰对象的功能。
另外一点,各个装饰器之间最好是完全独立的功能,不要有依赖,这样在进行装饰组合的时候,才没有先后顺序的限制,也就是先装饰谁和后装饰谁都应该是一样的,否则会大大降低装饰器组合的灵活性。
4. 装饰器和组件类的关系
装饰器是用来装饰组件的,装饰器一定要实现和组件类一致的接口,保证它们是同一个类型,并具有同一个外观,这样组合完成的装饰才能够递归的调用下去。
组件类是不知道装饰器的存在的,装饰器给组件添加功能是一种透明的包装,组件类毫不知情。需要改变的是外部使用组件类的地方,现在需要使用包装后的类,接口是一样的,但是具体的实现类发生了改变。
# 3.2 优缺点
1. 比继承更灵活
从为对象添加功能的角度来看,装饰模式比继承来得更灵活。继承是静态的,而且一旦继承是所有子类都有一样的功能。而装饰模式采用把功能分离到每个装饰器当中,然后通过对象组合的方式,在运行时动态的组合功能,每个被装饰的对象,最终有哪些功能,是由运行期动态组合的功能来决定的。
2. 更容易复用功能
装饰模式把一系列复杂的功能,分散到每个装饰器当中,一般一个装饰器只实现一个功能,这样实现装饰器变得简单,更重要的是这样有利于装饰器功能的复用,可以给一个对象增加多个同样的装饰器,也可以把一个装饰器用来装饰不同的对象,从而复用装饰器的功能。
3. 简化高层定义
装饰模式可以通过组合装饰器的方式,给对象增添任意多的功能,因此在进行高层定义的时候,不用把所有的功能都定义出来,而是定义最基本的就可以了,可以在使用需要的时候,组合相应的装饰器来完成需要的功能。
4. 会产生很多细粒度对象
前面说了,装饰模式是把一系列复杂的功能,分散到每个装饰器当中,一般一个装饰器只实现一个功能,这样会产生很多细粒度的对象,而且功能越复杂,需要的细粒度对象越多。
# 3.3 思考装饰模式
1. 装饰模式的本质
装饰模式的本质:功能细化,动态组合。
动态是手段,组合才是目的。这里的组合有两个意思,一个是动态功能的组合,也就是动态进行装饰器的组合;另外一个是指对象组合,通过对象组合来实现为被装饰对象透明的增加功能。
但是要注意,装饰模式不仅仅可以增加功能,也可以控制功能的访问,可以完全实现新的功能,还可以控制装饰的功能是在被装饰功能之前还是之后来运行等。
总之,装饰模式是通过把复杂功能简单化,分散化,然后在运行期间,根据需要来动态组合的这么一个模式。
2. 何时选用装饰模式
建议在如下情况中,选用装饰模式:
如果需要在不影响其它对象的情况下,以动态、透明的方式给对象添加职责,可以使用装饰模式,这几乎就是装饰模式的主要功能
如果不合适使用子类来进行扩展的时候,可以考虑使用装饰模式,因为装饰模式是使用的“对象组合”的方式。所谓不适合用子类扩展的方式,比如:扩展功能需要的子类太多,造成子类数目呈爆炸性增长。
# 3.4 相关模式
1. 装饰模式与适配器模式
这是两个没有什么关联的模式,放到一起来说,是因为它们有一个共同的别名:Wrapper。
这两个模式功能上是不一样的,适配器模式是用来改变接口的,而装饰模式是用来改变对象功能的。
2. 装饰模式与组合模式
这两个模式有相似之处,都涉及到对象的递归调用,从某个角度来说,可以把装饰看成是只有一个组件的组合。
但是它们的目的完全不一样,装饰模式是要动态的给对象增加功能;而组合模式是想要管理组合对象和叶子对象,为它们提供一个一致的操作接口给客户端,方便客户端的使用。
3. 装饰模式与策略模式
这两个模式可以组合使用。
策略模式也可以实现动态的改变对象的功能,但是策略模式只是一层选择,也就是根据策略选择一下具体的实现类而已。而装饰模式不是一层,而是递归调用,无数层都可以,只要组合好装饰器的对象组合,那就可以依次调用下去,所以装饰模式会更灵活。
而且策略模式改变的是原始对象的功能,不像装饰模式,后面一个装饰器,改变的是经过前一个装饰器装饰过后的对象,也就是策略模式改变的是对象的内核,而装饰模式改变的是对象的外壳。
这两个模式可以组合使用,可以在一个具体的装饰器里面使用策略模式,来选择更具体的实现方式。比如前面计算奖金的另外一个问题就是参与计算的基数不同,奖金的计算方式也是不同的。举例来说:假设张三和李四参与同一个奖金的计算,张三的销售总额是2万元,而李四的销售额是8万元,它们的计算公式是不一样的,假设奖金的计算规则是,销售额在5万以下,统一3%,而5万以上,5万内是4%,超过部分是6%。
参与同一个奖金的计算,这就意味着可以使用同一个装饰器,但是在装饰器的内部,不同条件下计算公式不一样,那么怎么选择具体的实现策略呢?自然使用策略模式就好了,也就是装饰模式和策略模式组合来使用。
4. 装饰模式与模板方法模式
这是两个功能上有相似点的模式。
模板方法模式主要应用在算法骨架固定的情况,那么要是算法步骤不固定呢,也就是一个相对动态的算法步骤,就可以使用装饰模式了,因为在使用装饰模式的时候,进行装饰器的组装,其实也相当于是一个调用算法步骤的组装,相当于是一个动态的算法骨架。
既然装饰模式可以实现动态的算法步骤的组装和调用,那么把这些算法步骤固定下来,那就是模板方法模式实现的功能了,因此装饰模式可以模拟实现模板方法模式的功能。
但是请注意,仅仅只是可以模拟功能而已,两个模式的设计目的、原本的功能、本质思想等都是不一样的。
# 四:JDK
- java.io.BufferedInputStream(InputStream) (opens new window)
- java.io.DataInputStream(InputStream) (opens new window)
- java.io.BufferedOutputStream(OutputStream) (opens new window)
- java.util.zip.ZipOutputStream(OutputStream) (opens new window)
- java.util.Collections#checkedList|Map|Set|SortedSet|SortedMap (opens new window)