第九章 组合模式 - 树形结构的处理
一、杀毒软件的框架结构
1. 介绍
开发一个杀毒软件,既可以对某个文件夹(Folder)杀毒,也可以对指定文件(File)进行杀毒。
2. 面向对象的解法
按照面向对象的设计思路,应该在 Folder 中包含图像、文本等文件类型的集合,以及文件夹的集合。
class ImageFile {
// image property
}
class TextFile {
// text property
}
class Folder {
List<ImageFile> images = new ArrayList<>();
List<TextFile> texts = new ArrayList<>();
List<Folder> folders = new ArrayList<>();
void addFolder();
void addImage();
void addText();
// ... remove/find etc.
}
3. 存在的问题
- Folder 的设计和实现十分复杂,需要定义多个集合存储不同类型的成员,且需要针对不同成员提供各自的 增删改查方法,存在大量冗余代码,系统维护困难
- 由于系统没有抽象层,客户端必须有区别地对待 文件夹和各类型的文件,无法对其进行统一处理
- 系统灵活性和可扩展性差,增加新类型的叶子和容器都需要对原有代码进行修改
4. 解决思路
整个架构中包含两类不同的元素,文件夹和文件。文件夹中可以包含文件或文件夹、而文件不能再包含子文件或子文件夹。因此,我们可以将文件夹成为 容器(Container),不同类型的文件是其成员,也称为 叶子(Leaf)。
二、组合模式
1. 定义
对于树形结构,当容器对象(如文件夹)的某一个方法被调用时,将遍历整个属性结构,其中使用了递归调用的机制来对整个结构进行处理。由于容器对象和叶子对象在功能上的区别,在使用时必须有区别地对待。而大多数情况下我们希望一致地处理它们,因为对于这些对象的使用具有一致性。
组合模式:组合多个对象形成树形结构,以表示具有“整体 - 部分” 关系的层次结构。组合模式对单个对象(叶子对象)和组合对象(容器对象)的使用具有一致性,组合模式又可以成为“整体 - 部分”模式,属于对象结构性模式。
2. 组合模式中的几个角色
- Component - 抽象构件
- 接口或抽象类,为 Leaf 和 Composite 对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现
- Leaf - 叶子构件
- 叶子节点,没有子节点,实现了在抽象构件中定义的行为
- 对于访问及管理子构件的方法,可以通过异常等方式进行处理
- Composite - 容器构件
- 容器节点对象,包含子节点
- 子节点可以是叶子节点,也可以是容器节点
- 提供一个集合用于存储子节点,实现了抽象构件中定义的行为,包含那些访问及管理子节点的方法
- 在业务方法中可以递归调用子节点的业务方法
三、 完整解决方案
// 组合模式
abstract class AbstractFile {
public abstract void add(AbstractFile file);
public abstract void remove(AbstractFile file);
public abstract AbstractFile getChild(int i);
public abstract void killVirus();
}
class ImageFile extends AbstractFile {
private String name;
public ImageFile(String name) {
this.name = name;
}
@Override
public void add(AbstractFile file) {
System.out.println("不支持该方法!");
}
@Override
public void remove(AbstractFile file) {
System.out.println("不支持该方法!");
}
@Override
public AbstractFile getChild(int i) {
System.out.println("不支持该方法!");
return null;
}
@Override
public void killVirus() {
System.out.println("对图像进行[" + name + "]杀毒");
}
}
class TextFile extends AbstractFile {
private String name;
public TextFile(String name) {
this.name = name;
}
@Override
public void add(AbstractFile file) {
System.out.println("不支持该方法!");
}
@Override
public void remove(AbstractFile file) {
System.out.println("不支持该方法!");
}
@Override
public AbstractFile getChild(int i) {
System.out.println("不支持该方法!");
return null;
}
@Override
public void killVirus() {
System.out.println("对文本进行[" + name + "]杀毒");
}
}
class Folder extends AbstractFile {
private List<AbstractFile> files = new ArrayList<>();
private String name;
public Folder(String name) {
this.name = name;
}
@Override
public void add(AbstractFile file) {
files.add(file);
}
@Override
public void remove(AbstractFile file) {
files.remove(file);
}
@Override
public AbstractFile getChild(int i) {
return files.get(i);
}
@Override
public void killVirus() {
System.out.println("对文件夹[" + name + "]进行杀毒");
for (AbstractFile file : files) {
file.killVirus();
}
}
}
public class CompositePattern {
public static void main(String[] args) {
AbstractFile folder1 = new Folder("图像文件夹");
AbstractFile folder2 = new Folder("文本文件夹");
AbstractFile file1 = new ImageFile("图片一");
AbstractFile file2 = new ImageFile("图片二");
AbstractFile file3 = new ImageFile("文本一");
AbstractFile file4 = new ImageFile("文本二");
folder1.add(file1);
folder1.add(file2);
folder2.add(file3);
folder2.add(file4);
folder2.add(folder1);
folder2.killVirus();
}
}
四、透明组合模式和安全组合模式
通过引入 组合模式,该杀毒软件具有良好的可扩展性,在新增文件类型时,只需要新增一个文件类继承 AbstractFile 即可。
但是 Abstract 中声明了大量用于管理和访问成员构件的方法,如 add() remove() 等方法,提供对应的错误提示和异常处理。
解决方案一:将叶子构件的add()、remove() 方法移至AbstractFile中,提供默认实现:
abstract class AbstractFile {
public void add(AbstractFile file){
System.out.println("对不起,不支持该方法!")
}
public void remove(AbstractFile file){
System.out.println("对不起,不支持该方法!")
}
public AbstractFile getChild(int i){
System.out.println("对不起,不支持该方法!")
}
public abstract void killVirus();
}
解决方案二:不提供 add()、remove() 等抽象方法,由具体需要的子构件自己提供。但是会导致客户端不得不使用容器类本身来声明容器构件对象,否则无法访问其中新增的方法。客户端代码无法通过容器构件的抽象构件来定义。
1. 透明组合模式
上面的解决方案一,就是透明组合模式的实现。由抽象构件提供子构件方法的默认实现。
好处是确保所有构件类都有相同的接口,对客户端来说,叶子对象和容器对象的方法一致。
缺点是,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一层对象,因此不应该有 add()/remove() 等方法,在运行时可能会出现异常。
2. 安全组合模式
解决方案二,就是安全组合模式的实现。抽象构件中不声明管理成员对象的方法,而是在 Composite类中声明并实现这些方法。
这种做法是安全的,因为叶子对象没有了管理成员对象的方法,客户端也就不能对叶子对象调用这些方法了。
五、总结
组合模式使用面向对象的思想来实现树形结构的构建与处理,描述了如何将容器对象和叶子对象进行递归组合,实现简单,灵活型号。
优点
- 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,使客户端忽略了层次的差异,方便对整个层次结构进行控制
- 客户端可以一致地使用一个组合结构或其中的单个对象,不必关系处理的是单个对象还是整个组合结构,简化客户端代码
- 组合模式为树形结构的面向对象实现,提供了一种灵活的解决方案,通过叶子对象和容器对象的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单
缺点
- 增加新构件时很难对容器中的构件类型进行限制
适用场景
- 在具有整合和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们
- 在一种使用面向对象语言开发的系统中需要处理一个树形结构
- 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型
六、练习
Sunny软件公司欲开发一个界面控件库,界面控件分为两大类,一类是单元控件,例如按钮、文本框等,一类是容器控件,例如窗体、中间面板等,试用组合模式设计该界面控件库。
/**
* 组合模式:
* Sunny软件公司欲开发一个界面控件库,
* 界面控件分为两大类,一类是单元控件,例如按钮、文本框等,
* 一类是容器控件,例如窗体、中间面板等,
* 试用组合模式设计该界面控件库。
*/
abstract class UI {
abstract void display();
void add(UI ui){
System.out.println("不支持该方法");
}
}
class ButtonUI extends UI {
@Override
void display() {
System.out.println("显示按钮");
}
}
class InputUI extends UI {
@Override
void display() {
System.out.println("显示输入框");
}
}
class Window extends UI{
List<UI> uis = new ArrayList<>();
@Override
void display() {
System.out.println("--开始显示窗口--");
for (UI ui : uis) {
ui.display();
}
}
@Override
void add(UI ui) {
uis.add(ui);
}
}
class Dashboard extends UI {
List<UI> uis = new ArrayList<>();
@Override
void display() {
System.out.println("--开始显示面板--");
for (UI ui : uis) {
ui.display();
}
}
@Override
void add(UI ui) {
uis.add(ui);
}
}
public class CompositePattern {
public static void main(String[] args) {
final UI ui1 = new ButtonUI();
final UI ui2 = new ButtonUI();
final UI ui3 = new InputUI();
final UI ui4 = new InputUI();
final UI window = new Window();
final UI dashboard = new Dashboard();
dashboard.add(ui1);
dashboard.add(ui4);
window.add(ui1);
window.add(ui3);
window.add(ui2);
window.add(dashboard);
window.display();
}
}