aoi学院

Aisaka's Blog, School of Aoi, Aisaka University

设计模式-20软工第6周安排-结构型模式-组合模式、装饰模式

师说

树形结构不论在生活中或者是开发中都是一种非常常见的结构,一个容器对象(如文件夹)下可以存放多种不同的叶子对象或者容器对象,容器对象与叶子对象之间属性差别可能非常大。组合模式为解决此类问题而诞生,它可以让叶子对象和容器对象的使用具有一致性。而组合模式的使用场景就是出现树形结构的地方。比如:文件目录显示,多及目录呈现等树形结构数据的操作。比如:Swing中,Button、Checkbox等组件都是树叶,而Container容器是树枝;比如:文本编辑时,可以单个字编辑,也可以整段编辑,还可以全文编辑;文件复制时,可以一个一个文件复制,也可以整个文件夹复制等等情况。

下面我们以公司的层级结构为例,(选自《大话设计模式》)。

这幅图表示的是部分与整体的关系,因此我们就可以考虑使用组合模式。另外,细细分析图中的结构与组成,我们可以得出以下UML图:

组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构。


也可以看我大三以前的上这门课时的博客:

设计模式-结构型模式-结构型模式概述
设计模式-结构型模式-组合模式
设计模式-结构型模式-装饰器模式


组合模式

案例:公司的人事管理,就是领导与被领导。

根据这个人事图,最先设计的 UML 图是:

有没有发现,接口里有好多方法重复了呢?

改进版的 UML 看起来很不错额,最起码干掉了一个 IRoot 接口,整个类图看起来简洁很多而且方法也不重复,但仔细看看,真的改好了吗?

现在我们能理解了吧,上一个类图中 ILeaf 接口中的 getInfo() 方法有重大的嫌疑,你再回去仔细瞧瞧

那现在这个类图行了吗?这个时候我们的理解一下接口的作用了:
接口的作用:定义共性,凡是其他接口有的,另一个接口就没必要重复了。

上一图中 ICop 中 getInfo() 是不是和 IBranch 中的 getSubordinateInfo() 功能上重复了,所以还得改,最终才有这个版本的类图。

也许你已经发现,怎么和文章中的第一个图那么相似呢?

对的,这个就是组合模式图了,也就是说,如果你把自己的项目优化优化,也许不知不觉中就在使用了组合模式。


组合模式的几个角色:

  • 抽象构件(Component):定义参加组合的对象的共有方法和属性,可以定义一些默认的行为或属性,比如 getInfo
  • 叶子构件(Leaf):叶子对象,其下没有分支。
  • 树枝构件(Composit):树枝对象,它的作用是组合树枝节点和叶子节点。

组合的适用场合:

  • 你想表示对象的部分 - 整体层次结构。
  • 你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象

优点:

  • 组合模式可以很容易的增加新的构件。
  • 使用组合模式可以使客户端变的很容易设计,因为客户端可以对组合和叶节点一视同仁。

缺点:

  • 使用组合模式后,控制树枝构件的类型不太容易。
  • 用继承的方法来增加新的行为很困难。

PS:如果不太明白组合是什么,可以先了解一下组合与继承


装饰模式

定义

装饰模式(Decorator)动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式比生成子类更加灵活。

这里可以举一个生活中的例子,一个蛋糕,在蛋糕上摆上水果,这个蛋糕就变成了水果蛋糕,给这个水果蛋糕插上蜡烛,它就变成了一个生日蛋糕。(这是Head First中的一个例子,个人觉得非常的形象,记忆犹新)。


分析

如果我们需要扩展一个类的功能,你会怎么做呢?如果直接修改这个类,我们就违反了开闭原则(对修改关闭,对扩展开放)。

我们可以继承这个类,写一个这个类的子类,用于实现扩展功能,也可以使用组合(将这个类作为成员变量),达到相同的效果。

我们将继承这种关系称为is-a,组合这种关系称为use-a。is-a的耦合程度要高于use-a,所以我们经常可以听到这样的说法:组合优于继承,原因就是这两种关系的耦合程度不同。

装饰模式的核心思想,其实就是用组合代替继承。


图解

这里可以看出,装饰者模式的特点是,装饰者实现了被装饰对象的接口,同时又将被装饰对象作为了成员变量


实例

这里举一个咖啡店的例子,咖啡的原料是咖啡豆,我们可以使用咖啡豆和牛奶、蜂蜜、摩卡组合出不同价格、不同口味的咖啡。

这里咖啡豆就是被装饰的对象,也就是图示中的ConcreteComponent,饮品类就是我们抽象出的Component,定义了展示价格和材料两个方法。牛奶、蜂蜜、摩卡是装饰对象,也就是图中的ConcreteDecoratorA、ConcreteDecoratorB。他们抽象出的Decorator,同样定义了展示价格和材料两个方法,具体类图与实现如下:


代码

1
2
3
4
5
6
7
8
9
/**
* 饮品
*/
public interface Beverage {
/** 获取描述 */
String getDescription();
/** 获取金额 */
double getPrice();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 咖啡豆1
*/
public class CoffeeBean1 implements Beverage {

@Override
public String getDescription() {
return "第一种咖啡豆";
}

@Override
public double getPrice() {
return 10d;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 咖啡豆2
*/
public class CoffeeBean2 implements Beverage{
@Override
public String getDescription() {
return "第一种咖啡豆";
}

@Override
public double getPrice() {
return 12.5d;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 装饰器
*/
public class Decorator implements Beverage{

protected Beverage coffee;

@Override
public String getDescription() {
return "装饰器,由子类重写方法";
}

@Override
public double getPrice() {
return 0;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 蜂蜜
*/
public class Honey extends Decorator {
public Honey(Beverage coffee) {
this.coffee = coffee;
}

@Override
public String getDescription() {
return coffee.getDescription() + "加蜂蜜";
}

@Override
public double getPrice() {
return coffee.getPrice() + 4.5d;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 牛奶
*/
public class Milk extends Decorator {
public Milk(Beverage coffee) {
this.coffee = coffee;
}

@Override
public String getDescription() {
return coffee.getDescription() + "加牛奶";
}

@Override
public double getPrice() {
return coffee.getPrice() + 1.5d;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 摩卡
*/
public class Mocha extends Decorator {
public Mocha(Beverage coffee) {
this.coffee = coffee;
}

@Override
public String getDescription() {
return coffee.getDescription() + "加摩卡";
}

@Override
public double getPrice() {
return coffee.getPrice() + 2.5d;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 测试类
*/
public class Main {
public static void main(String[] args) {
CoffeeBean1 coffee1 = new CoffeeBean1();
CoffeeBean2 coffee2 = new CoffeeBean2();

// 加蜂蜜
Beverage honey = new Honey(coffee1);
// 加摩卡
Beverage mocha = new Mocha(honey);
System.out.println(mocha.getDescription());
System.out.println(mocha.getPrice());

// 加牛奶
Milk milk = new Milk(coffee2);
System.out.println(milk.getDescription());
System.out.println(milk.getPrice());
}
}

结果:

1
2
3
4
第一种咖啡豆加蜂蜜加摩卡
17.0
第一种咖啡豆加牛奶
14.0

参考资料

20软工第6周安排
组合模式(Composite Pattern )
三分钟带您搞懂装饰模式