概述
概念
组合模式是一种结构型设计模式,它允许将对象组合成树形结构来表示“部分-整体”的层次关系。组合模式允许客户端统一处理单个对象和组合对象,使得客户端可以将它们视为相同的数据类型。
在组合模式中,有两种主要类型的对象:叶子对象和容器对象。叶子对象是组合的最小单位,它不包含任何子对象;而容器对象包含叶子对象和/或其他容器对象,从而形成了一个递归的树形结构。
主要角色
- Component:组合中的对象声明接口,它定义了组合对象的基本行为,包括添加和删除组成部分等操作。
- Leaf:叶子是组合模式中的基本角色,它表示组合对象中的单个对象,通常是不可变的。叶子通常没有子节点,它们只包含一些基本的属性和行为。
- Composite:是组合模式中的核心角色,它由多个组件构成,可以包含叶子节点和非叶子节点。组合对象是可变的,它们可以通过添加或删除组成部分来改变自己的状态。组合对象负责管理自己的组成部分,并提供一些特定于组合对象的操作,如添加或删除组成部分等。
叶子节点和非叶子节点都是组件的一种,它们都实现了 Component 接口。
在使用组合模式时,客户端需要知道如何处理组合对象和单个对象,以及如何使用抽象组件中定义的接口来访问它们的行为。具体组件和抽象组件则负责实现组合对象和单个对象的具体行为。组合对象则负责管理它包含的组成部分,并提供一些特定于组合对象的操作。
应用场景
- 文件系统中的文件和文件夹
- 菜单中的菜单项和菜单
- 网页中的页面和页面元素
组合模式的实现
组合模式的实现需要定义一个抽象基类和两个具体的实现类。抽象基类中定义了组合对象和单个对象的共同接口,具体实现类中分别实现了组合对象和单个对象的具体行为。
类图
NS图
基本代码
抽象类
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 void display(int depth); }
组合对象
public class Composite extends Component { public ArrayList<Component> children = new ArrayList<Component>(); 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 void display(int depth) { //显示枝结点名称 for (var i = 0; i < depth; i++) { System.out.print("-"); } System.out.println(name); //对其下级进行遍历 for (Component item : children) { item.display(depth + 2); } } }
叶子节点
public class Leaf extends Component{ public Leaf(String name){ super(name); } //为了消除叶子结点和枝结点的区别,所以他们具备相同的接口,即便叶子结点不需要这两个功能 @Override public void add(Component component) { System.out.println("不能添加叶子了"); } @Override public void remove(Component component) { System.out.println("不能移除叶子"); } @Override public void display(int depth) { //叶子结点的显示方法,只显示其名称和级别 for(var i=0;i<depth;i++){ System.out.print("-");} System.out.println(name); } }
客户端
public class Client { public static void main(String[] args) { Composite root=new Composite("root"); root.add(new Leaf("Leaf A")); root.add(new Leaf("Leaf B")); Component comp=new Composite("composite X"); comp.add(new Leaf("Leaf XA")); comp.add(new Leaf("Leaf XB")); root.add(comp); Component comp2=new Composite("composite XY"); comp2.add(new Leaf("Leaf XYA")); comp2.add(new Leaf("Leaf XYB")); comp.add(comp2); Leaf leaf=new Leaf("leaf C"); root.add((leaf)); Leaf leaf2=new Leaf("leaf D"); root.add((leaf2)); root.remove(leaf2); root.display(1); } }
输出结果
组合模式的精髓
在于将单个对象和组合对象统一对待,从而使得客户端可以一视同仁地操作它们,而无需关心其具体类型。这种设计思想体现了面向对象设计中的几个重要原则和模式:
- 透明性:组合模式通过将组合对象和叶子对象抽象成统一的组件类,让客户端对它们进行透明处理。客户端不需要知道正在处理的是叶子对象还是组合对象,只需通过统一的接口与它们交互。
- 单一职责原则:组合模式将对象的结构和行为分离开来,每个对象专注于自己的职责。叶子对象负责完成具体的操作,而组合对象负责管理子对象。
- 易扩展性:当系统需要增加新的叶子对象或组合对象时,组合模式无需修改现有的客户端代码,而是通过扩展组件类来实现。这提高了系统的灵活性和可扩展性。
- 清晰的层次结构:组合模式能够清晰地描述对象的层次结构,使得系统的整体结构更加直观和易于理解。
在代码中的体现:
客户端在创建对象时,无论是Leaf还是Composite,使用相同的构造函数或方法来创建对象,比如new Leaf(“Leaf A”)和new Composite(“composite X”)。
在添加子对象时,客户端使用相同的方法add来添加子对象,无论是添加Leaf还是添加Composite对象。
在移除对象时,客户端同样使用相同的方法remove来移除对象,无论是移除Leaf还是移除Composite对象。
这些操作都体现了客户端代码以统一的方式处理单个对象和对象的组合,而无需关心它们具体是哪一种类型。这种设计使得客户端代码更加简洁、清晰,并且更容易适应系统变化。
这样的原因除了因为Composite和leaf都继承于Component。也可以从树的角度进行思考,比如下图,当把组合对象以及下面的叶对象看出一个整体,对于根组合对象来说,也可以当成一个叶对象。
意外收获(❀❀)
调试代码中,还看到了一个特别有意思的现象:
创建Composite对象的时候,先走父类构造方法,此时children:null
走完父类构造之后,先走了public ArrayList children = new ArrayList();之后children:size=0,再这之后才结束构造方法
children的变化说明了什么呢?为什么会这样呢?
因为在Java中,当一个对象被创建时,成员变量会先被初始化为默认值,对于引用类型来说,默认值是null。这也解释了为什么在调试时会看到children开始是null。
children的变化如下:
首先,children作为Composite的成员变量被初始化为null。
然后,会调用Composite父类的构造函数super(name)来初始化父类的属性和状态。
最后,执行Composite类的构造函数体中的其他代码,在这里手动将children赋值为new ArrayList()来创建一个新的ArrayList对象并赋给children。
因此,在调试时会看到children开始是null,然后在执行完父类构造函数之后,children才不是null。
最后想说:
在Java中,类的实例化过程分为以下几个步骤:
1、所有的成员变量(包括实例变量和类变量)都会先被初始化为默认值。对于引用类型来说,默认值是null,对于数值类型来说,默认值是0或0.0,布尔类型的默认值是false。
2、然后会调用相应的构造函数进行初始化,其中可以在构造函数中对成员变量进行进一步赋值或初始化操作。
在调试时看到children开始是null,正是因为它在实例化时被初始化为默认值null。而在执行完父类构造函数后,手动将children赋值为new ArrayList(),从而创建了一个新的ArrayList对象并赋给children。
再加一条,作为一个带着叶子节点的Composite,是组合好一家子之后才往父节点挂靠的,也就和前面我说的作为一个整体可以看成一个叶子。
具体代码如下:
示意图:
虽然代码中取名children,不要理解成子节点,举个例子,如果Composite是妈妈,children不是孩子,而是子宫,子宫用来添加孩子(叶子),说这个的原因,是因为我被开始的自己的理解蠢哭了
应用示例-公司组织架构管理
需求
使用组合模式管理公司组织架构可以帮助公司更好地组织和管理其内部的部门、员工以及其它相关的业务实体。业务需求层级结构管理:公司内部通常存在多层级的组织结构,包括总公司、分公司、部门、小组等。需要一个灵活的方式来管理这种层级关系,使得可以方便地进行增加、删除、修改各个层级的组织单元。
结构图
代码
抽象公司类
abstract class Company { protected String name; public Company(String name ){ this.name=name; } public abstract void add(Company company); public abstract void remove(Company company); public abstract void display(int dept); public abstract void lineOfDuty();//履行职责,不同部门需履行不同的职责 }
组织类
public class ConcreteCompany extends Company { protected ArrayList<Company> children = new ArrayList<Company>(); public ConcreteCompany(String name) { super(name); } @Override public void add(Company company) { children.add(company); } @Override public void remove(Company company) { children.remove(company); } @Override public void display(int dept) { for (var i = 0; i < dept; i++) System.out.print("-"); System.out.println(name); for (Company item : children) { item.display(dept + 2);//为什么+2 } } @Override public void lineOfDuty() { for (Company item : children) { item.lineOfDuty(); } } }
财务部门
public class FinanceDepartment extends Company{ public FinanceDepartment(String name){ super(name); } @Override public void add(Company company) { } @Override public void remove(Company company) { } @Override public void display(int dept) { for (var i = 0; i <dept ; i++) { System.out.print("-"); } System.out.println(name); } @Override public void lineOfDuty() { System.out.println(name+"公司财务管理"); } }
人事部门
public class HRDepartment extends Company{ public HRDepartment(String name){ super(name); } @Override public void add(Company company) { } @Override public void remove(Company company) { } @Override public void display(int dept) { for (var i = 0; i <dept ; i++) { System.out.print("-"); } System.out.println(name); } @Override public void lineOfDuty() { System.out.println(name+"员工招聘培训管理"); } }
客户端
public class Client { public static void main(String[] args) { Company root=new ConcreteCompany("北京总公司"); root.add(new HRDepartment("总公司人力资源部")); root.add(new FinanceDepartment("总公司财务部")); Company comp=new ConcreteCompany("西南分公司"); comp.add(new HRDepartment("西南分公司人力资源部")); comp.add(new FinanceDepartment("西南分公司财务部") ); root.add(comp); Company comp2=new ConcreteCompany("成都分公司"); comp2.add(new HRDepartment("成都分公司人力资源部")); comp2.add(new FinanceDepartment("成都分公司财务部") ); comp.add(comp2); Company comp3=new ConcreteCompany("重庆分公司"); comp3.add(new HRDepartment("重庆分公司人力资源部")); comp3.add(new FinanceDepartment("重庆分公司财务部") ); comp.add(comp3); System.out.println("结构图:"); root.display(1); System.out.println("职责"); root.lineOfDuty(); } }
输出结果
组合模式的优缺点
优点
- 可以将复杂的层次结构变得简单化
- 可以统一处理组合对象和单个对象
- 可以提高代码的复用性和可维护性
缺点
- 对于大型的层次结构,组合模式可能会导致性能问题
- 组合模式的实现需要定义多个类,增加了代码量
- 组合模式的理解和实现需要一定的设计模式知识
总结
组合模式是一种常用的设计模式,它可以将复杂的层次结构变得简单化,并且可以统一处理组合对象和单个对象。通过组合模式,可以将客户端代码与对象的层次结构分离,客户端只需要面向抽象构件(Component)进行操作,无需关心具体是哪种类型的对象。
总的来说,组合模式的核心思想是将对象组织成树形结构,并提供统一的接口,使得客户端可以一致地处理单个对象和对象的组合。这种模式在处理具有层次结构的对象时非常有用,如组织架构、文件系统等。