0%

GoF设计模式(十):Composite Pattern 组合模式

Composite Pattern 组合模式,作为一种结构型设计模式,其通常适用于具有树形结构的场景

abstract.jpeg

基本原理

现实世界中树形结构非常常见,比如公司的部门-人员的组织架构、磁盘目录下的文件夹-文件结构、XML文件等。在树形结构下其通常有两种类型节点:叶子节点、树枝节点。以磁盘目录为例,叶子节点即文件,树枝节点即文件夹。在软件开发中,如果对这两种类型节点分别处理,将会变得十分麻烦。那么有没有办法可以使得对这两种类型节点的处理可以统一呢?答案就是组合模式,通过组合多个对象形成树形结构来表示”整体—部分”关系的层次结构,其对叶子节点和树枝节点的使用具有一致性

组合模式的角色比较简单,其只有三种角色:

  • 抽象组件角色:为了保证客户端对两种类型节点的使用的一致性,我们需要定义一个二者公共的接口或抽象类,其声明了二者共有的方法、属性。即下文示例的FileSystem类
  • 叶子组件角色:其是对抽象组件角色的具体实现。显然对于一个叶子节点而言,其下是没有子节点的。即下文示例的File类
  • 树枝组件角色:其亦是对抽象组件角色的实现,其表示的是树枝类型节点。其内部会有一个容器用于存放子节点,同时支持添加、删除等一系列对子节点的操作。即下文示例的Folder类

实现

透明组合模式

现在,让我们通过Java来实现文件系统的文件夹-文件的组织结构,来帮助大家更好的理解组合模式。首先,定义一个公共接口——FileSystem类。其中定义了文件(即叶子节点)、文件夹(即树枝节点)中所需的属性和方法

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
/**
* 抽象组件角色: 文件系统
*/
public abstract class FileSystem {
/**
* 文件/文件夹名
*/
private String name;

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

public void getInfo() {
System.out.println("[NAME]: " + name);
}

/**
* 向文件夹中添加
* @param fileSystem
*/
abstract public void add(FileSystem fileSystem);

/**
* 从文件夹中移除
* @param fileSystem
*/
abstract public void remove(FileSystem fileSystem);

/**
* 获取该文件夹下的全部子节点(文件/文件夹)
* @return
*/
abstract public List<FileSystem> getChildren();
}

然后对于叶子类型节点的File类而言,对于不支持的方法(因这些方法属于文件夹的操作),可选择抛出异常或者空实现,这里我们选择前者,以便当client对文件对象调用不支持的操作时进行提醒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 叶子组件: 文件
*/
public class File extends FileSystem {
public File(String name) {
super(name);
}

@Override
public void add(FileSystem fileSystem) {
throw new UnsupportedOperationException("不支持的操作");
}

@Override
public void remove(FileSystem fileSystem) {
throw new UnsupportedOperationException("不支持的操作");
}

@Override
public List<FileSystem> getChildren() {
throw new UnsupportedOperationException("不支持的操作");
}
}

而对于树枝组件Folder文件夹类,我们则需要在内部持有容器来存放子节点,并实现文件夹操作子节点的相关方法

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 FileSystem {
/**
* 内部持有容器以保存子节点
*/
private List<FileSystem> list = new LinkedList<>();

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

/**
* 向文件夹中添加
* @param fileSystem
*/
@Override
public void add(FileSystem fileSystem) {
list.add(fileSystem);
}

/**
* 从文件夹中移除
* @param fileSystem
*/
@Override
public void remove(FileSystem fileSystem) {
list.remove(fileSystem);
}

/**
* 获取该文件夹下的全部子节点(文件/文件夹)
* @return
*/
@Override
public List<FileSystem> getChildren() {
return list;
}
}

至此磁盘目录的文件夹-文件树状结构就已经实现完成了,现在我们来看看client的使用姿势。同时也一并提供了遍历某个目录下的所有节点的showTree方法

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
/**
* Composite Pattern 组合模式 Demo: 透明模式
*/
public class TransparencyCompositePatternDemo {
public static void main(String[] args) {
Folder root = new Folder("863重点项目");

// 添加一级节点(文件/文件夹)
File file1 = new File("README.md");
File file2 = new File("LICENSE.txt");
Folder folderC = new Folder("C代码");
Folder folderJava = new Folder("Java代码");
root.add(file1);
root.add(file2);
root.add(folderC);
root.add(folderJava);

// 向 <C代码> 文件夹添加C源码文件
File fileC1 = new File("main.c");
File fileC2 = new File("test.c");
folderC.add(fileC1);
folderC.add(fileC2);

// 向 <Java代码> 文件夹添加Java源码文件
File fileJava1 = new File("webSocketServer.java");
File fileJava2 = new File("tcpServer.java");
File fileJava3 = new File("updServer.java");
folderJava.add(fileJava1);
folderJava.add(fileJava2);
folderJava.add(fileJava3);

// 遍历该目录下的全部节点(文件/文件夹)
System.out.println("基于透明模式的组合模式");
showTree(root);
}

/**
* 遍历该文件夹下的全部节点(文件/文件夹)
* @param root
*/
public static void showTree(FileSystem root) {
for( FileSystem fileSystem : root.getChildren() ) {
fileSystem.getInfo();
if( fileSystem instanceof Folder) {
showTree(fileSystem);
}
}
}
}

测试结果如下,符合测试预期。在本实现方案中,我们将仅适用于文件夹类的操作方法也一并放在抽象类进行声明,故该实现通常被称为透明组合模式。对于Client而言,操作File、Folder对象时必须保证节点类型判断无误、处理得当,否则会导致运行期发生意外,即所谓的不安全

figure 1.png

安全组合模式

透明组合模式最大的问题在于不够安全,而解决该问题也很简单。我们将抽象组件角色FileSystem类中关于文件夹(即树枝节点)的方法声明移除掉,而仅仅在树枝组件Folder文件夹类中定义它们。这样一旦client期望通过叶子节点File类非法调用它们,即会导致编译期错误,便于及时发现、排查、修复。这就是所谓的安全组合模式。现在,我们的抽象组件角色FileSystem类中就仅仅包含叶子、树枝节点共有的方法了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 抽象组件角色: 文件系统
*/
public abstract class FileSystem {
private String name;

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

public void getInfo() {
System.out.println("[NAME]: " + name);
}
}

而叶子节点File类自然也精简很多,没有那些冗余的方法定义了

1
2
3
4
5
6
7
8
/**
* 叶子组件: 文件
*/
public class File extends FileSystem {
public File(String name) {
super(name);
}
}

最后只在树枝组件Folder文件夹类中定义其特有的相关方法即可

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
/**
* 树枝组件: 文件夹
*/
public class Folder extends FileSystem {
private List<FileSystem> list = new LinkedList<>();

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

/**
* 向文件夹中添加
* @param fileSystem
*/
public void add(FileSystem fileSystem) {
list.add(fileSystem);
}

/**
* 从文件夹中移除
* @param fileSystem
*/
public void remove(FileSystem fileSystem) {
list.remove(fileSystem);
}

/**
* 获取该文件夹下的全部子节点(文件/文件夹)
* @return
*/
public List<FileSystem> getChildren() {
return list;
}
}

至此,一个安全组合模式就已经实现完毕了。下面让我们通过如下测试用例来看看效果

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
public class SafetyCompositePatternDemo {
public static void main(String[] args) {
Folder root = new Folder("863重点项目");

// 添加一级节点(文件/文件夹)
File file1 = new File("README.md");
File file2 = new File("LICENSE.txt");
Folder folderC = new Folder("C代码");
Folder folderJava = new Folder("Java代码");
root.add(file1);
root.add(file2);
root.add(folderC);
root.add(folderJava);

// 向 <C代码> 文件夹添加C源码文件
File fileC1 = new File("main.c");
File fileC2 = new File("test.c");
folderC.add(fileC1);
folderC.add(fileC2);

// 向 <Java代码> 文件夹添加Java源码文件
File fileJava1 = new File("webSocketServer.java");
File fileJava2 = new File("tcpServer.java");
File fileJava3 = new File("updServer.java");
folderJava.add(fileJava1);
folderJava.add(fileJava2);
folderJava.add(fileJava3);

// 遍历该目录下的全部节点(文件/文件夹)
System.out.println("基于安全模式的组合模式");
showTree(root);
}

/**
* 遍历该文件夹下的全部节点(文件/文件夹)
* @param root
*/
public static void showTree(Folder root) {
for( FileSystem fileSystem : root.getChildren() ) {
fileSystem.getInfo();
if( fileSystem instanceof Folder) {
showTree( (Folder) fileSystem);
}
}
}
}

测试结果如下,符合预期

figure 2.png

参考文献

  1. Head First 设计模式 弗里曼著
请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝