【设计模式(八)】结构型模式之组合模式

it2026-04-02  6

个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道

如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充

前言

这个是我随手截的文件目录,这样的结构都很眼熟吧?

一个个文件,组成一个文件夹,文件和文件夹又可以组成更大的文件夹,进而形成一个树形结构

那么,我们在点开的时候,需要先确认目标是文件夹或者文件,文件就打开,文件夹则是展开下一级

也就是说我们对于"部分"与“整体”采取了不同的方案,但是这样带来了一些不必要的麻烦,我们只想打开这个目标,具体怎么打开那是你们自己的事情

进而言之,客户只关心对目标进行操作,并不关心因为目标不同而导致的差异。用户对于目标的“部分”与“整体”是一致对待的。

比如,删除目标,客户只需要点击删除即可,并不关心具体逻辑,实际上如果是文件就直接删除,如果是文件夹需要先删除下一层级的所有目标。

复制、粘贴、移动这类操作同理

换个例子,我们告诉一个部门明天放假,只需要告知负责人即可

至于这个部门,包括一整个公司分部,还是研发部,还是研发小组,还是说就负责人一个人,我么并不需要关心,这是负责人该关心的事情

事实上如果这个部门不止负责人一个人的话,他大概率也是转告下一层部门的负责人而已(套娃??)

因此,你看,我们告知一个人,或者告知任何一个部门,都是一样的,并不需要先确定是哪层的负责人

这就是组合模式,又称部分整体模式,用于将一组相似的对象作为单一的对象整体,进而将部分与整体构造成树形结构。

这种模式创建了一个包含自己对象组的类,并提供操作相同对象组的方式。

组合模式定义了如何将容器对象和叶子对象进行递归组合,使得客户在使用的过程中无须进行区分,可以对他们进行一致的处理

1.介绍

使用目的:将对象组合成树形结构以表示"部分-整体"的层次结构,进而使客户可能够对单体对象或者组合对象的使用具有一致性

使用时机:希望用户能够忽略组合对象与单个对象的区别,进行统一的处理

解决问题:将“部分”与“整体”区别对待会带来不必要的麻烦

实现方法:将容器对象和叶子对象进行递归组合,使得客户在使用的过程中无须进行区分,可以对他们进行一致的处理

应用实例:

对于文件/文件夹的删除、复制、剪切、粘贴、移动等操作向一个个人/部门传递消息或者指令,只需要告知负责人即可需求展示一个无限层级的目录,如图书管理系统(曾经遇到的需求,层级未知,最后干脆做成了无限层级)

优点:

客户对于“部分”和“整体”的操作具有一致性,无疑提高了用户体验高层代码调用简单方便,也简化了客户端代码节点自由度增加,可以选择仅变更自己,或变更所有子节点组合内部增加新的节点很方便,不需要修改结构的源代码,满足“开闭原则”

缺点:

所有节点都是实现类,而不是接口,违背了依赖倒置原则设计较为复杂,需要理清不同层级之间的关系难以使用集成的方法进行扩展

2.结构

主要包含3个角色

抽象构件(Component)角色:声明树枝节点和叶子节点的公共接口,并实现默认行为。

根据是否声明访问和管理子类的接口,分为透明模式和安全模式

树叶构件(Leaf)角色:组合中的叶节点对象,它没有子节点,用于实现抽象构件角色中声明的公共接口。

树枝构件(Composite)角色:组合中的分支节点对象,它有子节点。它实现了抽象构件角色中声明的接口,同时还需要存储和管理子节点。

其实也可以将三者融为一体,一个类就搞定了,但是不利于扩展,功能较少的时候可以这样做

图就不花了,这套娃的结构根本无从下手

3.实现

这里给出三种示例,分别是简单组合模式、透明模式和安全模式

3.1.简单组合模式

不利于扩展,但代码简单,适用于功能较少的机构

之所以列出来说,是因为这种其实才是最常见的,尤其是算法中经常用到,包括链表也是使用的这种结构

3.1.1.示例1

模拟业务如下:

链表由节点连接构成每个节点存储一个值和下一个节点,下一个节点可能为空

代码很简单直接贴了,经常刷算法题的都能默写下来了

package com.company.test.composite; import lombok.Data; @Data class Node { public int val; public Node next; public Node(int val, Node next) { this.val = val; this.next = next; } public void show() { if (next == null) { //为最后一个节点,打印本身的值并转行 System.out.println(val); } else { //不为最后一个节点,打印本身的值,并打印下一个节点 System.out.print(val + " -> "); next.show(); } } } public class SimpleCompositeTest { public static void main(String[] args) { Node node1 = new Node(1, null); Node node2 = new Node(2, node1); Node node3 = new Node(3, node2); Node node4 = new Node(4, node3); node1.show(); node2.show(); node3.show(); node4.show(); } }

运行结果

我们在这里舍弃了抽象构建,而且树叶构件和树枝构件使用同一个类实现即可,通过next是否为空判断是否是叶子节点

3.1.2.示例2

模拟业务如下

职员信息包括4个数据:姓名、职位、薪水、下级人员职员信息提供接口进行打印,可以打印当前职员信息,也可以同时打印所有下级人员信息

结构与示例1类似,故不多做解释

package com.company.test.composite; import lombok.Data; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @Data class Employee { private String name; private String dept; private int salary; private List<Employee> subordinate; public Employee(String name, String dept, int salary, List<Employee> subordinate) { this.name = name; this.dept = dept; this.salary = salary; this.subordinate = subordinate; } public String toString() { String subordinateNames = subordinate.stream().map(Employee::getName).collect(Collectors.joining(", ")); return ("Employee :[ " + "Name : " + name + ", dept : " + dept + ", salary : " + salary + ", subordinate : " + subordinateNames + " ]"); } public void showCurrent() { System.out.println(this.toString()); } public void showAll() { showCurrent(); subordinate.forEach((m) -> { m.showAll(); }); } } public class SimpleCompositeTest1 { public static void main(String[] args) { Employee clerk1 = new Employee("clerk1", "clerk", 10000, new ArrayList<>()); Employee clerk2 = new Employee("clerk2", "clerk", 10000, new ArrayList<>()); Employee manager1 = new Employee("manager1", "manager", 50000, Arrays.asList(new Employee[]{clerk1, clerk2})); Employee manager2 = new Employee("manager2", "manager", 50000, new ArrayList<>()); Employee ceo = new Employee("Jobs", "ceo", 150000, Arrays.asList(new Employee[]{manager1, manager2})); ceo.showAll(); } }

运行结果

3.2.透明组合模式

透明模式是把组合使用的方法放到抽象类中,不管叶子对象还是树枝对象都有相同的结构

这样做的好处就是叶子节点和树枝节点对于外界没有区别,它们具备完全一致的行为接口。

但因为Leaf类本身不具备add()、remove()方法的功能,所以实现它是没有意义的

定义抽象构件角色Component

abstract class Component { protected String name; public Component(String name) { this.name = name; } public abstract void add(Component component); public abstract void remove(Component component); public abstract List<Component> getChildren(); public abstract void show(int depth); }

定义树叶构件Leaf

class Leaf extends Component { public Leaf(String name) { super(name); } @Override public void add(Component component) { //空实现,抛出“不支持请求”异常 throw new UnsupportedOperationException(); } @Override public void remove(Component component) { //空实现,抛出“不支持请求”异常 throw new UnsupportedOperationException(); } @Override public List<Component> getChildren() { return null; } @Override public void show(int depth) { while (depth-- > 0) { System.out.print("-"); } System.out.println(name); } }

定义树枝构件Composite

class Composite extends Component { List<Component> children = new ArrayList<>(); public Composite(String name) { super(name); } @Override public void add(Component component) { children.add(component); } @Override public void remove(Component component) { children.remove(component); } @Override public List<Component> getChildren() { return children; } @Override public void show(int depth) { int nowDepth = depth; while (depth-- > 0) { System.out.print("-"); } System.out.println(name); children.forEach(m -> { m.show(nowDepth + 1); }); } }

完整代码

package com.company.test.composite; import java.util.ArrayList; import java.util.List; abstract class Component { protected String name; public Component(String name) { this.name = name; } public abstract void add(Component component); public abstract void remove(Component component); public abstract List<Component> getChildren(); public abstract void show(int depth); } class Leaf extends Component { public Leaf(String name) { super(name); } @Override public void add(Component component) { //空实现,抛出“不支持请求”异常 throw new UnsupportedOperationException(); } @Override public void remove(Component component) { //空实现,抛出“不支持请求”异常 throw new UnsupportedOperationException(); } @Override public List<Component> getChildren() { return null; } @Override public void show(int depth) { while (depth-- > 0) { System.out.print("-"); } System.out.println(name); } } class Composite extends Component { List<Component> children = new ArrayList<>(); public Composite(String name) { super(name); } @Override public void add(Component component) { children.add(component); } @Override public void remove(Component component) { children.remove(component); } @Override public List<Component> getChildren() { return children; } @Override public void show(int depth) { int nowDepth = depth; while (depth-- > 0) { System.out.print("-"); } System.out.println(name); children.forEach(m -> { m.show(nowDepth + 1); }); } } public class ClearCompositeTest { public static void main(String[] args) { Component leaf1 = new Leaf("leaf1"); Component leaf2 = new Leaf("leaf2"); Component composite1=new Composite("composite1"); composite1.add(leaf1); composite1.add(leaf2); Component leaf3 = new Leaf("leaf3"); Component composite3=new Composite("composite3"); composite3.add(composite1); composite3.add(leaf3); composite3.show(1); } }

运行结果

如图,组装了一个目录,并将其打印出来

可以看到,树叶和树枝拥有同样的功能,但树叶的部分功能并没有正常执行(抛出异常或空实现),这样会带来安全性问题

安全组合模式就是为了解决这种情况

3.3.安全组合模式

在该方式中,将管理子构件的方法移到树枝构件中,抽象构件和树叶构件没有对子对象的管理方法

这样就避免了上一种方式的安全性问题,但由于叶子和分支有不同的接口,客户端在调用时要知道树叶对象和树枝对象的存在,所以失去了透明性

结构一样的,就直接贴代码了,自己对比一下

package com.company.test.composite; import java.util.ArrayList; import java.util.List; abstract class Component { protected String name; public Component(String name) { this.name = name; } public abstract void add(Component component); public abstract void remove(Component component); public abstract List<Component> getChildren(); public abstract void show(int depth); } class Leaf extends Component { public Leaf(String name) { super(name); } @Override public void add(Component component) { //空实现,抛出“不支持请求”异常 throw new UnsupportedOperationException(); } @Override public void remove(Component component) { //空实现,抛出“不支持请求”异常 throw new UnsupportedOperationException(); } @Override public List<Component> getChildren() { return null; } @Override public void show(int depth) { while (depth-- > 0) { System.out.print("-"); } System.out.println(name); } } class Composite extends Component { List<Component> children = new ArrayList<>(); public Composite(String name) { super(name); } @Override public void add(Component component) { children.add(component); } @Override public void remove(Component component) { children.remove(component); } @Override public List<Component> getChildren() { return children; } @Override public void show(int depth) { int nowDepth = depth; while (depth-- > 0) { System.out.print("-"); } System.out.println(name); children.forEach(m -> { m.show(nowDepth + 1); }); } } public class ClearCompositeTest { public static void main(String[] args) { Component leaf1 = new Leaf("leaf1"); Component leaf2 = new Leaf("leaf2"); Component composite1=new Composite("composite1"); composite1.add(leaf1); composite1.add(leaf2); Component leaf3 = new Leaf("leaf3"); Component composite3=new Composite("composite3"); composite3.add(composite1); composite3.add(leaf3); composite3.show(1); } }

运行结果

4.透明组合模式与安全组合模式的区别

透明模式:

只需要在定义的时候确定是树叶或者树枝,使用的时候树叶和树枝可以当做同一个对象使用树叶实现了所有功能,但部分功能实际上并不拥有,需要抛出异常或者空实现,会带来安全性问题

安全模式:

使用时需要知道是树叶或者树枝,部分功能可能存在差异所有功能都正常实现了,所以不会带来透明模式的安全性问题因为需要知道是节点类型,使用不便,一定程度上违背了初衷

简单点说,一种是叶节点与树枝节点具备一致的行为接口但有空实现的透明模式,另一种是树枝节点单独拥有用来组合的方法但调用不便的安全模式

使用哪种,自行取舍咯,如果是图方便,简单组合模式就可以满足很多需求,如果需要保证安全,就需要使用安全组合模式,但是最符合初衷的应该是透明组合模式

5.扩展使用

5.1.将节点进一步抽象化

模拟文件夹目录,包括文件夹和文件

package com.company.test.composite; import lombok.Data; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @Data abstract class Files { protected String name; public Files(String name) { this.name = name; } public abstract void check(); public abstract void copyFiles(); } abstract class File extends Files { public File(String name) { super(name); } @Override public void copyFiles() { System.out.println("copy file: " + name); } } class Text extends File { public Text(String text) { super(text); } @Override public void check() { System.out.println("show text: " + name); } } class Mp3 extends File { public Mp3(String name) { super(name); } @Override public void check() { System.out.println("play mp3: " + name); } } class Folder extends Files { public Folder(String name) { super(name); } List<Files> subordinateFiles = new ArrayList<>(); public void addFiles(Files files) { subordinateFiles.add(files); } public void removeFiles(Files files) { subordinateFiles.remove(files); } @Override public void check() { String subordinateFileNames = subordinateFiles.stream().map(m -> m.getName()).collect(Collectors.joining(", ")); System.out.println("open folder: " + name + ", subordinateFiles: " + subordinateFileNames); } @Override public void copyFiles() { subordinateFiles.forEach(m -> { m.copyFiles(); }); System.out.println("copy folder: " + name); } } public class FileCompositeTest { public static void main(String[] args) { Text text = new Text("HelloWorld.text"); Mp3 mp3 = new Mp3("我在昨天的梦里又看见了你.mp3"); Text lyric = new Text("我在昨天的梦里又看见了你.text"); Folder folder = new Folder("我在昨天的梦里又看见了你"); folder.addFiles(mp3); folder.addFiles(lyric); Folder folder1 = new Folder("empty"); Folder root = new Folder("root"); root.addFiles(folder); root.addFiles(folder1); root.addFiles(text); System.out.println("<---------------------------操作文件夹:root------------------------------->"); root.check(); root.copyFiles(); System.out.println("<---------------------------操作文件夹:我在昨天的梦里又看见了你------------------------------->"); folder.check(); folder.copyFiles(); System.out.println("<---------------------------操作文件:我在昨天的梦里又看见了你.mp3------------------------------->"); mp3.check(); mp3.copyFiles(); } }

运行结果

文件目录(模拟)

看,无论是mp3文件,或者text文件,或者folder文件夹,我们都可以执行同样的check()和copyFiles()操作

其余扩展使用后续再追加,暂时只想到这里就写到这里

后记

将相似的目标提取其共同点,从而可以进行部分一致性操作,而目标本身只需要关注自己的特点,将共同点交由接口或者父类处理

其实这也是多态和继承的目的,所以从学习Java开始,我们其实就在按照这种思想设计程序,组合模式不过是其中一种方案而已

作者:Echo_Ye

WX:Echo_YeZ

Email :echo_yezi@qq.com

个人站点:在搭了在搭了。。。(右键 - 新建文件夹)

最新回复(0)