本篇Blog继续学习结构型模式,了解如何更优雅的布局类和对象。结构型模式描述如何将类或对象按某种布局组合以便获得更好、更灵活的结构。虽然面向对象的继承机制提供了最基本的子类扩展父类的功能,但结构型模式不仅仅简单地使用继承,而更多地通过组合与运行期的动态组合来实现更灵活的功能。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。本篇学习的是组合模式。由于学习的都是设计模式,所有系列文章都遵循如下的目录:
- 模式档案:包含模式的定义、模式的特点、解决什么问题、优缺点、使用场景等
- 模式结构:包含模式的角色定义及调用关系以及其模版代码
- 模式示例:包含模式的实现方式代码举例,生活中的简单问题映射
- 模式实践:如果工作中或开源项目用到了该模式,就将使用过程贴到这里,并且客观讨论使用的是否恰当
- 模式对比:如果模式相似,有必要体现其相似点及不同点,区分使用,说明哪些场景下使用哪种模式比较好
- 模式扩展:如果模式有与标准结构定义不同的变体形式,一并体现出其变体结构
接下来所有设计模式的介绍都暂且遵循此基本行文逻辑吗,如果某一条目没有则无需体现,但条目顺序遵循此结构
模式档案
在现实生活中,存在很多部分-整体的关系,例如,大学中的部门与学院、总公司中的部门与分公司等等,在软件开发中也是这样,例如,文件系统中的文件与文件夹、窗体程序中的简单控件与容器控件等。对这些简单对象与复合对象的处理,如果用组合模式来实现会很方便。
模式定义:组合模式将一组对象组织(Compose)成树形结构,以表示一种部分 - 整体的层次结构。组合让客户端调用方可以统一单个对象和组合对象的处理逻辑
优点:组合模式有如下两个优点:
- 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,简化了客户端代码
- 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足开闭原则
缺点:使用场景较窄,只有处理的数据对象为树形结构才能发挥作用,通常可以应用于组织架构设计。
使用场景:数据必须能表示成树形结构才能使用组合模式,这也导致了这种模式在实际的项目开发中并不那么常用。但是,一旦数据满足树形结构,应用这种模式就能发挥很大的作用,能让代码变得非常简洁
组合模式跟面向对象设计中的组合关系(通过组合来组装两个类),完全是两码事。这里讲的组合模式,主要是用来处理树形结构数据。这里的数据,可以简单理解为一组对象集合
模式结构
组合模式包含以下主要角色。
- 抽象组件(Component)角色:它的主要作用是为树叶组件和树枝组件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象组件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝组件完成。(总的抽象类或接口,定义一些通用的方法,比如新增、删除)
- 树叶组件(Leaf)角色:是组合中的叶节点对象,它没有子节点,用于继承或实现抽象组件。
- 树枝组件(Composite)角色 / 中间组件:是组合中的分支节点对象,它有子节点,用于继承和实现抽象组件。它的主要作用是存储和管理子部件,通常包含
Add()、Remove()、GetChild()
等方法。
组合模式分为透明式的组合模式和安全式的组合模式。透明式的组合模式结构如下:
1 透明式组合模式
在该方式中,由于抽象构件声明了所有子类中的全部方法,所以客户端无须区别树叶对象和树枝对象,对客户端来说是透明的。但其缺点是:树叶构件本来没有 Add()、Remove() 及 GetChild()
方法,却要实现它们(空实现或抛异常),这样会带来一些安全性问题
可以看到树叶组件也实现了管理的方法,但是方法为空方法
2 安全式组合模式
在该方式中,将管理子构件的方法移到树枝构件中,抽象构件和树叶构件没有对子对象的管理方法,这样就避免了上一种方式的安全性问题,但由于叶子和分支有不同的接口,客户端在调用时要知道树叶对象和树枝对象的存在,所以失去了透明性
透明式和安全式的组合模式,唯一的区别就是抽象组件中是否添加树叶节点的管理方法:
- 透明式添加,那么在基于接口进行使用时,使用者并不需要关心实际对象是树枝还是树叶
- 组合式不添加,那么在基于接口进行使用时,要想使用树叶管理方法,还得声明对象为树枝对象,因为树叶对象并不具备管理方法。
这是二者的一个本质区别。
模式实现
分别实现透明式组合模式和安全式组合模式
透明式组合模式
各个角色代码如下:
1 抽象组件
//抽象构件 interface Component { public void add(Component c); public void remove(Component c); public Component getChild(int i); public void operation(); }
2 树叶组件
//树叶构件 class Leaf implements Component { private String name; public Leaf(String name) { this.name = name; } public void add(Component c) { } public void remove(Component c) { } public Component getChild(int i) { return null; } public void operation() { System.out.println("树叶" + name + ":被访问!"); } }
3 树枝组件
//树枝构件 class Composite implements Component { private ArrayList<Component> children = new ArrayList<Component>(); public void add(Component c) { children.add(c); } public void remove(Component c) { children.remove(c); } public Component getChild(int i) { return children.get(i); } public void operation() { for (Object obj : children) { ((Component) obj).operation(); } } }
客户端调用如下:
public class CompositePattern { public static void main(String[] args) { Component twig0 = new Composite(); Component twig1 = new Composite(); Component leaf1 = new Leaf("1"); Component leaf2 = new Leaf("2"); Component leaf3 = new Leaf("3"); twig0.add(leaf1); twig0.add(twig1); twig1.add(leaf2); twig1.add(leaf3); twig0.operation(); //空方法,不执行 leaf1.add(leaf2); } }
打印结果如下:
树叶1:被访问! 树叶2:被访问! 树叶3:被访问!
安全式组合模式
各个角色代码如下:
1 抽象组件
修改 Component 代码,只保留层次的公共行为
//抽象构件 interface Component { public void operation(); }
2 树叶组件
//树叶构件 class Leaf implements Component { private String name; public Leaf(String name) { this.name = name; } public void operation() { System.out.println("树叶" + name + ":被访问!"); } }
3 树枝组件
//树枝构件 class Composite implements Component { private ArrayList<Component> children = new ArrayList<Component>(); public void add(Component c) { children.add(c); } public void remove(Component c) { children.remove(c); } public Component getChild(int i) { return children.get(i); } public void operation() { for (Object obj : children) { ((Component) obj).operation(); } } }
修改客户端代码,将树枝构件类型更改为 Composite 类型,以便获取管理子类操作的方法
public class CompositePattern { public static void main(String[] args) { Composite twig0 = new Composite(); Composite twig1 = new Composite(); Component leaf1 = new Leaf("1"); Component leaf2 = new Leaf("2"); Component leaf3 = new Leaf("3"); twig0.add(leaf1); twig0.add(twig1); twig1.add(leaf2); twig1.add(leaf3); twig0.operation(); } }
模式实践
我们有如下两个场景的实践:设计一个文件系统和设计一个组织成本核算系统,以下实例组合模式为安全式组合模式。
设计一个文件系统
假设我们有这样一个需求:设计一个类来表示文件系统中的目录,能方便地实现下面这些功能:动态地添加、删除某个目录下的子目录或文件;统计指定目录下的文件个数;统计指定目录下的文件总大小。代码如下:
public class FileSystemNode { private String path; private boolean isFile; private List<FileSystemNode> subNodes = new ArrayList<>(); public FileSystemNode(String path, boolean isFile) { this.path = path; this.isFile = isFile; } public int countNumOfFiles() { if (isFile) { return 1; } int numOfFiles = 0; for (FileSystemNode fileOrDir : subNodes) { numOfFiles += fileOrDir.countNumOfFiles(); } return numOfFiles; } public long countSizeOfFiles() { if (isFile) { File file = new File(path); if (!file.exists()) return 0; return file.length(); } long sizeofFiles = 0; for (FileSystemNode fileOrDir : subNodes) { sizeofFiles += fileOrDir.countSizeOfFiles(); } return sizeofFiles; } public String getPath() { return path; } public void addSubNode(FileSystemNode fileOrDir) { subNodes.add(fileOrDir); } public void removeSubNode(FileSystemNode fileOrDir) { int size = subNodes.size(); int i = 0; for (; i < size; ++i) { if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) { break; } } if (i < size) { subNodes.remove(i); } } }
单纯从功能实现角度来说,上面的代码没有问题,已经实现了我们想要的功能。但是,如果我们开发的是一个大型系统,从扩展性(文件或目录可能会对应不同的操作)、业务建模(文件和目录从业务上是两个概念)、代码的可读性(文件和目录区分对待更加符合人们对业务的认知)的角度来说,我们最好对文件和目录进行区分设计,定义为 File 和 Directory 两个类
public abstract class FileSystemNode { protected String path; public FileSystemNode(String path) { this.path = path; } public abstract int countNumOfFiles(); public abstract long countSizeOfFiles(); public String getPath() { return path; } } public class File extends FileSystemNode { public File(String path) { super(path); } @Override public int countNumOfFiles() { return 1; } @Override public long countSizeOfFiles() { java.io.File file = new java.io.File(path); if (!file.exists()) return 0; return file.length(); } } public class Directory extends FileSystemNode { private List<FileSystemNode> subNodes = new ArrayList<>(); public Directory(String path) { super(path); } @Override public int countNumOfFiles() { int numOfFiles = 0; for (FileSystemNode fileOrDir : subNodes) { numOfFiles += fileOrDir.countNumOfFiles(); } return numOfFiles; } @Override public long countSizeOfFiles() { long sizeofFiles = 0; for (FileSystemNode fileOrDir : subNodes) { sizeofFiles += fileOrDir.countSizeOfFiles(); } return sizeofFiles; } public void addSubNode(FileSystemNode fileOrDir) { subNodes.add(fileOrDir); } public void removeSubNode(FileSystemNode fileOrDir) { int size = subNodes.size(); int i = 0; for (; i < size; ++i) { if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) { break; } } if (i < size) { subNodes.remove(i); } } }
设计一个组织成本核算系统
假设我们在开发一个 OA 系统(办公自动化系统)。公司的组织结构包含部门和员工两种数据类型。其中,部门又可以包含子部门和员工
我们希望在内存中构建整个公司的人员架构图(部门、子部门、员工的隶属关系),并且提供接口计算出部门的薪资成本(隶属于这个部门的所有员工的薪资和)。部门包含子部门和员工,这是一种嵌套结构,可以表示成树这种数据结构。计算每个部门的薪资开支这样一个需求,也可以通过在树上的遍历算法来实现。所以,从这个角度来看,这个应用场景可以使用组合模式来设计和实现
抽象组件:人力资源管理
public abstract class HumanResource { protected long id; protected double salary; public HumanResource(long id) { this.id = id; } public long getId() { return id; } public abstract double calculateSalary(); }
树叶节点:人员成本信息
public class Employee extends HumanResource { public Employee(long id, double salary) { super(id); this.salary = salary; } @Override public double calculateSalary() { return salary; } }
树枝节点:部门成本信息
public class Department extends HumanResource { private List<HumanResource> subNodes = new ArrayList<>(); public Department(long id) { super(id); } @Override public double calculateSalary() { double totalSalary = 0; for (HumanResource hr : subNodes) { //递归计算薪资 totalSalary += hr.calculateSalary(); } this.salary = totalSalary; return totalSalary; } public void addSubNode(HumanResource hr) { subNodes.add(hr); } }
客户端调用逻辑
public class Demo { private static final long ORGANIZATION_ROOT_ID = 1001; private DepartmentRepo departmentRepo; // 依赖注入 private EmployeeRepo employeeRepo; // 依赖注入 public void buildOrganization() { Department rootDepartment = new Department(ORGANIZATION_ROOT_ID); buildOrganization(rootDepartment); } // 构建组织架构的代码 private void buildOrganization(Department department) { List<Long> subDepartmentIds = departmentRepo.getSubDepartmentIds(department.getId()); for (Long subDepartmentId : subDepartmentIds) { Department subDepartment = new Department(subDepartmentId); department.addSubNode(subDepartment); //递归构建组织 buildOrganization(subDepartment); } List<Long> employeeIds = employeeRepo.getDepartmentEmployeeIds(department.getId()); for (Long employeeId : employeeIds) { double salary = employeeRepo.getEmployeeSalary(employeeId); department.addSubNode(new Employee(employeeId, salary)); } } }
总结一下
组合模式的场景使用较为特殊,实际上就是树这种数据结构组织的对象,通过抽象复杂数据结构中的通用方法使这个复杂对象调用起来和简单对象一样容易,例如核算一个部门的成本数据,对于部门+人这类组织数据就可以理所当然的认为是一个树形结构,无论是部门这种树枝还是人这种树叶都提供计算成本的统一方法,只要构建完毕这样一个组织,那么无论是计算哪一级的成本,都统一调用计算方法即可,递归逻辑已经内置在组织构建过程中了。