设计模式之--组合模式

一、组合模式的介绍
组合模式(Compisite Pattern)也称为部分整体模式(Part-Whole Pattern),结构型设计模式之一,它将一组相似的对象看作一个对象处理,并根据一个树状结构来组合对象,然后提供一个统一的方法去访问相应的对象,以此忽略掉对象与对象集合之间的差别。
生活中一个比较典型的例子就是组织结构的树状图如下:
设计模式之--组合模式
文章图片

设计模式之--组合模式
文章图片

二、组合模式的定义
将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
三、组合模式的使用场景

  • 【设计模式之--组合模式】表示对象的部分-整体层次结构时。
  • 从一个整体中能够独立出部分模块或功能的场景。
四、组合模式的UML类图
设计模式之--组合模式
文章图片

1、根据类图我们可以得出如下一个组合模式的通用代码:
(1)抽象根节点
public abstract class Component{ protected String name; //节点 public Component(String name){ this.name = name; } /** * 具体的逻辑方法由子类实现 */ public abstract void dosomething(); }

(2)具体枝干节点
public class Composite extends Component{ /** * 存储节点的容器 */ private List components = new ArrayList<>(); public Composite(String name){ super(name); }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 getChild(int index){ return components.get(index); } }

(3)具体叶子节点
public class Leaf extends Component{ public Leaf (String name){ super(name); }public void doSomething(){ System.out.println(name); } }

(4)客户类
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(); } }

(5)运行结果:
Root Branch1 Leaf1 Branch2 Leaf2

2、角色介绍
(1)Component: 抽象根节点,为组合中的对象声明接口。在适当的情况下,实现所有类共有接口的缺省行为。声明一个接口用于访问和管理 Component 的子节点。可在递归结构中定义一个接口,用于访问一个父节点,并在合适的情况下实现它。
(2)Composite:定义有子节点的那些枝干节点的行为,存储子节点,在 Component 接口中实现与子节点有关的操作。
(3)Leaf: 在组合中表示叶子节点对象,叶子节点没有子节点,在组合中定义节点对象的行为。
(4)通过Component 接口操纵组合节点的对象。
上述的UML类图和代码中有一个弊端,我们在Client客户类中直接使用了Component的实现类。
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"); } }

这与依赖倒置原则相违背,我们所定义的抽象Component 在这里起的作用就不大,既然是面向接口编程,那么我们就该把更多的焦点放在接口的设计上,如果我们稍微修改上面的类图将位于Composite中的一些实现方法定义到Component 中,那么我们会得到一个不一样的组合模式,如下图:
设计模式之--组合模式
文章图片

像这样将组合所使用的方法定义在抽象类的方式称为透明的组合模式,而上面我们所说的组合模式则称为安全的组合模式。透明组合模式中不管是叶子节点还是枝干节点都有着相同的结构,那意味着我们无法通过getChildren 方法的到子节点的类型,而必须在方法实现的内部进行判断,我们来看看这种方式的组合模式UML类图代码。
  • 透明的组合模式抽象根节点
public abstract class Component {protected String name; //节点public Component(String name){ this.name = name; } /** * 具体的逻辑方法由子类实现 */ public abstract void doSomething(); /** *添加子节点 * @param child 子节点 */ public abstract void addChild(Component child); /** *移除子节点 * @param child 子节点 */ public abstract void removeChild(Component child); /** *获取子节点 * @param index 子节点对应下标 * @return */ public abstract Component getChildren(int index); }

  • 透明的组合模式具体枝干节点
public class Composite extends Component {/** * 存储节点的容器 */ private List components = new ArrayList<>(); public Composite(String name) { super(name); }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); }public void doSomething() { System.out.println(name); }@Override public void addChild(Component child) { throw new UnsupportedOperationException("叶子节点没有子节点"); }@Override public void removeChild(Component child) { throw new UnsupportedOperationException("叶子节点没有子节点"); }@Override public Component getChildren(int index) { throw new UnsupportedOperationException("叶子节点没有子节点"); }}

  • 客户类
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(); } }

  • 运行结果
Root Branch1 Leaf1 Branch2 Leaf2

五、组合模式的实现
操作系统的文件系统其实就是一个典型的组合模式的例子,这里我们以此为例,看看一个简单的文件系统是如何构成的。
设计模式之--组合模式
文章图片

首先声明一个Dir抽象类表示文件和文件夹。
(1)表示文件和文件夹的抽象类
public abstract class Dir {/** * 声明一个List成员变量存储文件夹下的所有元素 */ protected List dirs = new ArrayList<>(); private String name; public Dir(String name) { this.name = name; }/** * 添加一个文件或文件夹 * * @param dir *文件或文件夹 */ public abstract void addDir(Dir dir); /** * 移除一个文件或文件夹 * * @param dir */ public abstract void rmDir(Dir dir); /** * 清空文件夹下所有元素 */ public abstract void clear(); /** * 输出文件夹目录结构 */ public abstract void print(); /** * 获取文件夹下所有的文件或子文件夹 * * @return 文件夹下所有的文件或子文件夹 */ public abstract List getFiles(); /** * 获取文件或文件夹名 * * @return */ public String getName() { return name; }}

在该抽象类中我们定义了相关的抽象方法,大家可以看到这里用到的就是所谓的透明的组合模式,这里要实现的功能很简单,知识简单地打印一些目录结构,因此,声明一个成员变量来存储当前文件夹或文件夹的名称并提供对应的getter方法。接下来就是具体的文件夹类,该类中我们实现具体的文件夹方法逻辑:
(2)表示文件夹的类
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 itera = dirs.iterator(); while (itera.hasNext()) { Dir dir = itera.next(); dir.print(); if (itera.hasNext()) { System.out.print(","); } } System.out.print(")"); }@Override public List getFiles() { return dirs; }}

像addDir、rmDir 在这里的方法中实现逻辑都比较简单,这里主要就是print方法用来输出文件夹的目录结构,该方法逻辑也比较简单,首先输出自己的名字,也就是当前文件夹名,然后迭代遍历 子元素,调用子元素的 print 方法输出其目录结构,如果遇到子元素还是个文件夹,那么递归遍历直至所有的输出元素均为文件为止。接下来看看文件类的实现。
(3)表示文件的类
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 getFiles() { throw new UnsupportedOperationException("文件对象不支持该操作"); }}

文件类相对于文件夹类来说既不支持添加也不支持删除,因为文件不能作为文件夹来使用,它本是文件系统中的最小分割单位,因此,这里我们虽然实现了Dir中的一些添加、删除等操作方法,但是,其实现逻辑只是抛出一个不支持异常,因为就文件本身来说就不支持对应的操作。最后,在一个客户类中构建目录结构并输出。
(4) 客户类
public class Client {public static void main(String[] args) { // 构造一个目录对象标识C盘根目录 Dir diskC = new Folder("C"); // 桌面目录下有一个文件ImbaMallLog.txt diskC.addDir(new File("ImbaMallLog.txt")); // 桌面目录下还有3个子目录Windows、PerfLogs、Program File Dir dirWin = new Folder("Windows"); // Windows目录下有文件explorer.ext dirWin.addDir(new File("explorer.ext")); diskC.addDir(dirWin); // PerfLogs目录 Dir dirPer = new Folder("PerfLogs"); // CarFactory目录下有文件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("build.gradle")); diskC.addDir(dirPro); // 打印出文件结构 diskC.print(); }}

输出结果如下:
C(ImbaMallLog.txt,Windows(explorer.ext),PerfLogs(null.txt),Program File(build.gradle))

这里我们以括号作为一个文件夹的内容范围,如上输出所示,C盘文件夹下有3个子文件夹Windows、PerfLogs、Program File以及一个文件ImbaMallLog.txt 而且3个子文件夹中还各自包含有子文件,一个典型的树状嵌套结构,这就是组合模式。

    推荐阅读