18 设计模式——组合模式

返回设计模式博客目录

介绍


组合(Composite)模式:将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性

组合模式也是结构模式之一,组合模式比较简单,它将一组相似的对象看作一个对象处理,并根据一个树状结构来组合对象,然后提供一个统一的方法去访问相应的对象,以此忽略掉对象与对象之间的差别。

优点

  • 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码。
  • 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”。

缺点

  • 设计较复杂,客户端需要花更多时间理清类之间的层次关系。
  • 不容易限制容器中的构件。
  • 不容易用继承的方法来增加构件的新功能。

使用场景

  • 表示对象的部分-整体层次结构时。
  • 从一个整体中能独立出部分模块或功能的场景。

结构与实现


模式包含以下主要角色。

  • Component:抽象根节点,为组合中的对象声明接口。
  • Composite:定义有子节点的那些枝干节点的行为,存储子节点,在 Component 接口中实现与子节点有关的操作。
  • Leaf:在组合中表示叶子节点对象。
  • Client:通过 Component 接口操作组合节点的对象。

其结构图如下图所示。

下面是它的模版代码:

1
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
public abstract class Component {
protected String name;
public Component(String name) {
this.name = name;
}
// 具体逻辑的方法由子类实现
public abstract void doSomething();
}
public class Composite extends Component {
/**
* 存储节点的容器
* @param name
*/
private List<Component> components = new ArrayList<>();
public Composite(String name) {
super(name);
}
@Override
public void doSomething() {
System.out.println(name);
if (null != components){
for (Component c : components) {
c.doSomething();
}
}
}
/**
* 添加子节点
* @param child
*/
public void addChild(Component child) {
components.add(child);
}
/**
* 移除子节点
* @param child
*/
public void removeChild(Component child) {
components.remove(child);
}
/**
* 获取子节点
* @param index
* @return
*/
public Component getChildren(int index) {
return components.get(index);
}
}
public class Leaf extends Component {
public Leaf(String name) {
super(name);
}
@Override
public void doSomething() {
System.out.print(name);
}
}

客户端测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Client {
public static void main(String[] args) {
// 构造一个根节点
Composite root = new Composite("Root");
// 构造两个枝干节点
Composite branch1 = new Composite("Branch1");
Composite branch2 = new Composite("Branch2");
// 构造两个叶子节点
Leaf leaf1 = new Leaf("Leaf1");
Leaf leaf2 = new Leaf("Leaf2");
// 将叶子节点添加至枝干节点中
branch1.addChild(leaf1);
branch2.addChild(leaf2);
// 将枝干节点添加至根节点中
root.addChild(branch1);
root.addChild(branch2);
// 执行方法
root.doSomething();
}
}

示例


下面我们以文件夹系统为例讲解一下组合模式的简单实现,整个文件夹系统如下所示:

首先声明一个抽象类,表示文件或文件夹:

1
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
public abstract class Dir {
/**
* 声明一个List成员变量存储文件夹下的所有元素
*/
protected List<Dir> dirs = new ArrayList<>();
private String name; //当前文件夹名
public Dir(String name) {
this.name = name;
}
/**
* 添加一个文件或文件夹
*/
public abstract void addDir(Dir dir);
/**
* 移除一个文件或文件夹
*/
public abstract void rmDir(Dir dir);
/**
* 清除文件夹下面的所有元素
*/
public abstract void clear();
/**
* 清空文件夹下的所有元素
*/
public abstract void print();
/**
* 获取文件夹下所有的文件或子文件夹
*/
public abstract List<Dir> getFiles();
/**
* 获取文件或文件夹名
*/
public String getName() {
return name;
}
}

然后定义文件夹类和文件类:

1
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
public class Folder extends Dir {
public Folder(String name) {
super(name);
}
@Override
public void addDir(Dir dir) {
dirs.add(dir);
}
@Override
public void rmDir(Dir dir) {
dirs.remove(dir);
}
@Override
public void clear() {
dirs.clear();
}
@Override
public void print() {
System.out.print(getName()+"(");
Iterator<Dir> iter = dirs.iterator();
while (iter.hasNext()){
Dir dir = iter.next();
dir.print();
if (iter.hasNext()){
System.out.print(",");
}
}
System.out.print(")");
}
@Override
public List<Dir> getFiles() {
return dirs;
}
}
public class File extends Dir {
public File(String name) {
super(name);
}
@Override
public void addDir(Dir dir) {
throw new UnsupportedOperationException("文件对象不支持该操作");
}
@Override
public void rmDir(Dir dir) {
throw new UnsupportedOperationException("文件对象不支持该操作");
}
@Override
public void clear() {
throw new UnsupportedOperationException("文件对象不支持该操作");
}
@Override
public void print() {
System.out.print(getName());
}
@Override
public List<Dir> getFiles() {
throw new UnsupportedOperationException("文件对象不支持该操作");
}
}

像这样将组合所使用的方法定义在抽象类的方式称为透明的组合模式,遵循了依赖倒置原则,但却违反了单一职责原则与接口隔离原则。

而前面模板代码中所描述的组合模式则称为安全地组合模式,这种模式客户端在使用的时候必须依赖具体的实现,这违反了依赖倒置原则,但遵循了单一职责原则与接口隔离原则。

测试代码:

1
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
public class Client {
public static void main(String[] args) {
// 构造一个目录对象表示 C 盘根目录
Dir diskC = new Folder("C");
// C 盘根目录下有一个文件 ImbaMallLog.txt
diskC.addDir(new File("ImbaMallLog.txt"));
// C 盘目录下还有3个子目录 Windows,PrefLogs,Program File
Dir dirWin = new Folder("Windows");
// Windows 目录下有文件 explorer.exe
dirWin.addDir(new File("explorer.exe"));
diskC.addDir(dirWin);
// PerfLogs 目录
Dir dirPer = new Folder("PerfLogs");
// PerfLogs 目录下有文件 null.txt
dirPer.addDir(new File("null.txt"));
diskC.addDir(dirPer);
// Program File 目录
Dir dirPro = new Folder("Program File");
// Program File 目录下有文件 ftp.txt
dirPro.addDir(new File("ftp.txt"));
diskC.addDir(dirPro);
// 打印出文件结构
diskC.print();
}
}

ANDROID 源码中的实现


Android 中的 View 和 ViewGroup 的嵌套组合是一个典型的组合模式实现,如下图所示。

在 Android 的这个视图层级中,容器一定是 ViewGroup,而且只有 ViewGroup 才能包含其他的 View,比如 LinearLayout 能包含 TextView、Button、CheckBox 等,但是反过来 TextView 是不能包含 LinearLayout 的,因为 TextView 直接继承于 View,其并非一个容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
...
public void addView(View child) {
addView(child, -1);
}
...
@Override
public void removeView(View view) {
if (removeViewInternal(view)) {
requestLayout();
invalidate(true);
}
}
...
public View getChildAt(int index) {
if (index < 0 || index >= mChildrenCount) {
return null;
}
return mChildren[index];
}
...
}

ViewGroup 是继承于 View 类的,但为什么有容器的功能呢?

1
2
3
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
...
}

从继承的角度来说,ViewGroup 拥有 View 类所有的非私有方法。既然如此,两者的差别就在于 ViewGroup 所实现的 ViewParent 和 ViewManager 接口上,而事实也是如此。

ViewManager 定义了 addView、removeView 等对子视图操作的方法。

1
2
3
4
5
public interface ViewManager {
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}

而 ViewParent 则定义了刷新容器的接口 requestLayout 和其他一些焦点事件的处理的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface ViewParent {
// 请求重新布局
public void requestLayout();
// 是否已经请求布局。这里需要注意,当我们调用 requestLayout 请求布局后,
// 这一过程并非是立即执行的,Android 会将请求布局的操作以消息的形式发送
// 至主线程的 Handler 并由其分发处理。因此在调用 requestLayout 方法请
// 求布局到布局真正接收到重新布局的命令时需要一段时间间隔
public boolean isLayoutRequested();
...
// 获取当前 View 的 ViewParent
public ViewParent getParent();
...
}

其中有一些方法比较常见,比如 requestLayout 和 bringChildToFront 等。

ViewGroup 除了所实现的这两个接口与 View 不一样外,还有重要的一点就是 ViewGroup 是抽象类,将 View 的 onLayout 重置为抽象方法。容器子类必须实现 onLayout 来布局定位。

除此之外,在 View 中比较重要的两个测绘流程的方法 onMeasure 和 onDraw 在 ViewGroup 中都没有被重写,相对于 onMeasure 方法,在 ViewGroup 中增加了一些计算子 View 的方法,如 measureChildren、measureChildrenWithMargins 等;而对于 onDraw 方法,ViewGroup 定义了一个 dispatchDraw 方法来调用其每一个子 View 的 onDraw 方法,由此可见,ViewGroup 真的就象一个容器一样,其职责只是负责对子元素的操作而非具体的个体行为。