设计模式十六之组合模式

通过理论,代码示例,Android源码来学习组合模式

介绍

组合模式(Composite Pattern)也称为部分整体模式 (Part Whole Pattern) ,属于结构性设计模式,组合模式比较简单,它将一组相似的对象看做一个对象处理,并根据一个树状结构来组合对象,然后提供一个统一的方法去访问相应的对象,以此忽略掉对象与对象集合之间的差别。

定义

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

使用场景

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

UML 类图

  • Component: 抽象根节点,为组合中的对象声明接口。在适当的情况下,实现所有类共有接口的缺省行为。声明一个接口用于访问和管理 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
public abstract class Component {


/**
* 节点名称
*/
protected String name;

public Component(String name) {
this.name = name;
}

/**
* 具体的逻辑方法由子类实现
*/
public abstract void doSomething();

/**
* 添加子节点
* @param component
*/
public abstract void addChild(Component component);

/**
* 移除子节点
* @param component
*/
public abstract void removeChild(Component component);

/**
* 获取子节点
* @param index
* @return
*/
public abstract Component getChildren(int index);

}

枝干节点:

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
public class Composite extends Component {
/**
* 存储节点的容器
*/
private List<Component> mLists = new ArrayList<>();


public Composite(String name) {
super(name);
}

@Override
public void doSomething() {
System.out.println("name: " + name);
if (mLists != null && mLists.size() > 0) {
for (Component component : mLists) {
component.doSomething();
}
}
}

@Override
public void addChild(Component component) {
mLists.add(component);
}

@Override
public void removeChild(Component component) {
mLists.remove(component);
}

@Override
public Component getChildren(int index) {
return mLists.get(index);
}
}

叶子节点:

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
public class Leaf extends Component {
public Leaf(String name) {
super(name);
}

@Override
public void doSomething() {
System.out.println("name: " + name);

}

@Override
public void addChild(Component component) {
throw new UnsupportedOperationException("叶子节点没有子节点");
}

@Override
public void removeChild(Component component) {
throw new UnsupportedOperationException("叶子节点没有子节点");
}

@Override
public Component getChildren(int index) {
throw new UnsupportedOperationException("叶子节点没有子节点");
}
}

test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void testComponent() {
//构造一个根节点
Component root = new Composite("Root");

//构造枝干节点
Component branchA = new Composite("branchA");
Component branchB = new Composite("branchB");

//构造叶子节点
Component leafA = new Leaf("leafA");
Component leafB = new Leaf("LeafB");

//将叶子节点添加至枝干节点中
branchA.addChild(leafA);
branchB.addChild(leafB);

//将枝干节点添加进跟节点
root.addChild(branchA);
root.addChild(branchB);

root.doSomething();
}

output:

1
2
3
4
5
name: Root
name: branchA
name: leafA
name: branchB
name: LeafB

实战

业务背景: 用代码表示文件夹跟文件的组合关系;

文件和文件夹抽象类:

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
public abstract class Dir {
/**
* 声明一个 List 成员变量存储文件夹下面所有元素
*/
protected List<Dir> mDirs = new ArrayList<>();

/**
* 当前文件或文件夹名称
*/
protected String name;

public Dir(String name) {
this.name = name;
}

/**
* 添加一个文件或文件夹
* @param dir
*/
public abstract void addDir(Dir dir);

/**
* 移除一个文件或文件夹
* @param dir
*/
public abstract void removeDir(Dir dir);

/**
* 清空文件夹下面所有的元素
*/
public abstract void clear();

/**
* 输出文件夹下目录结构
*/
public abstract void print();

/**
* 获取文件夹下所有的文件或子文件夹
*/
public abstract List<Dir> getFiles();

/**
* 获取文件或文件夹名称
* @return
*/
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
public class Folder extends Dir {
public Folder(String name) {
super(name);
}

@Override
public void addDir(Dir dir) {
mDirs.add(dir);

}

@Override
public void removeDir(Dir dir) {
mDirs.remove(dir);
}

@Override
public void clear() {
mDirs.clear();
}

@Override
public void print() {
System.out.print(getName() + " (");
Iterator<Dir> iterator = mDirs.iterator();
while (iterator.hasNext()) {
Dir dir = iterator.next();
dir.print();
if (iterator.hasNext()) {
System.out.print(", ");
}
}
System.out.print(")");
}

@Override
public List<Dir> getFiles() {
return mDirs;
}
}

具体文件类:

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
public class File extends Dir {
public File(String name) {
super(name);
}

@Override
public void addDir(Dir dir) {
throw new UnsupportedOperationException("文件对象不支持该操作");
}

@Override
public void removeDir(Dir dir) {
throw new UnsupportedOperationException("文件对象不支持该操作");
}

@Override
public void clear() {
throw new UnsupportedOperationException("文件对象不支持该操作");
}

@Override
public void print() {
System.out.println("name: "+name);
}

@Override
public List<Dir> getFiles() {
throw new UnsupportedOperationException("文件对象不支持该操作");
}
}

test:

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
@Test
public void testComponent2(){
//构造一个目录对象表示 sdcard 盘跟目录
Dir diskSdcard = new Folder("sdcard");

// sdcard 下有一个文件
diskSdcard.addDir(new File("bug.log"));

//sdcard 下还有一个子目录 android
Dir android = new Folder("android");
//android 目录下有一个目录
Dir data = new Folder("data");
data.addDir(new File("2019-09-14.log"));
android.addDir(data);
diskSdcard.addDir(android);

//sdcard 下还有一个子目录 baidu
Dir baidu = new Folder("baidu");
//android 目录下有一个目录
Dir cache = new Folder("cache");
cache.addDir(new File("offline.baidu"));
baidu.addDir(cache);
diskSdcard.addDir(baidu);

//sdcard 下还有一个子目录 downloads
Dir downloads = new Folder("downloads");
//android 目录下有一个目录
Dir f360 = new Folder("360");
f360.addDir(new File("360.log"));
downloads.addDir(f360);
diskSdcard.addDir(downloads);

//打印文件结构
diskSdcard.print();
}

output:

1
2
3
4
5
6
7
8
sdcard (
//文件
name: bug.log,
//文件夹
android (data (name: 2019-09-14.log)),
baidu (cache (name: offline.baidu)),
downloads (360 (name: 360.log))
)

这里我们以括号作为一个文件夹的内容范围,如上输出所以 adcard 下文件夹有 3 个子文件夹 Android、baidu、downloads 子文件夹,以及一个 bug.log 文件,而在 3 个子文件夹中还各自包含子文件夹和文件,一个典型的树状嵌套解耦,这就是一个组合模式。

总结

优点:

  • 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让高层模块忽略了层次的差异,方便对整个层次结构进行控制。
  • 高层模块可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了高层模块的代码。
  • 在组合模式中增加了新的枝干构件和叶子构件都很方便,无须对现有类库进行任何修改,符合 “开闭原则”。
  • 组合模式为树状结构的面向对象实现提供了一种灵活的解决方案,通过叶子对象和树干对象的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。

缺点:

早新增构件时不好对树干中的构件类型进行限制,不能依赖类型系统来施加这些约束,因为在大多数情况下,它们都来自于想听的抽象层,此时,必须进行类型检查来实现,这个实现过程较为复杂。

坚持原创技术分享,您的支持将鼓励我继续创作!