1.组合模式概述
对于树形结构,比如文件目录,一个文件夹中可以包含多个文件夹和文件,而一个文件中不能在包含子文件或者子文件夹,在这里可以称文件夹为容器,称文件为叶子。
在树形结构中,当容器对象(比如文件夹)的某个方法被调用时,将遍历整个文件夹,寻找也包含这个方法的成员对象(容器对象或叶子对象)并调用执行。由于容器对象以及叶子对象在功能上的区别,使用这些对象的代码中必须有区别对待容器对象以及叶子对象,但大多数情况下需要一致性处理它们。
组合模式为解决此类问题而生,它可以让叶子对象以及容器对象的使用具有一致性。
1.1定义
组合模式:组合多个对象形成树形结构以表示具有“整体-部分”关系的层次结构。组合模式对单个对象(叶子对象)和组合对象(容器对象)的使用具有一致性。 组合模式又叫“部分-整体”模式,它是一种对象结构型模式。
1.2组合模式类图
组合模式的关键是定义了一个抽象构件类,它既可以表示叶子也可以表示容器,客户端针对该抽象构件进行编程,无须知道到底是叶子还是容器,同时容器对象与抽象构件之间需要建立一个聚合关联关系,在容器对象中既可以包含叶子也可以包含容器,以此实现递归组合,形成树形结构。
角色分配:
- Component(抽象构件):可以是接口或者抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现(业务方法)。在抽象构件中定义了访问以及管理它的子构件的方法(管理构件方法),例如增加/删除/获取子构件
- Leaf(叶子构件):表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为,对于访问以及管理子构件的方法,通常会抛出异常。(实现业务方法,调用管理构件方法抛出异常或提示)
- Composite(容器构件):表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括访问以及管理子构件的方法。(内含集合存储抽象构件,实现业务方法以及构件管理方法)
通用步骤
首先需要定义抽象构件类
- 定义抽象构件:定义抽象构件类,添加四个基本方法:增加/删除/获取成员+业务方法,可以将抽象构件类定义为抽象类或者接口。(定义业务方法以及构件管理方法)
- 定义叶子构件:继承或实现抽象构件类,覆盖或实现具体业务方法,同时对于管理或访问子构件的方法提供异常处理或错误提示。(实现业务方法)
- 定义容器构件:继承或实现抽象构件类,覆盖或实现抽象构件中的所有方法,一般来说容器构件会包含一个集合私有成员用于保存抽象构件,在业务方法中对这个集合进行遍历从而实现递归调用。(实现抽象构件所有方法)
1.2.1 抽象构件
抽象构件一般定义如下:
abstract class Component
{
abstract void add(Component c);
abstract void remove(Component c);
abstract Component getChild(int i);
abstract void operation();
}
1.2.2 叶子构件
class Leaf extends Component
{
public void add(Component c)
{
//叶子构件不能访问该方法
System.out.println("错误,不能访问添加构件方法!");
}
public void remove(Component c)
{
//叶子构件不能访问该方法
System.out.println("错误,不能访问删除构件方法!");
}
public Component getChild(int i)
{
//叶子构件不能访问该方法
System.out.println("错误,不能访问获取构件方法!");
return null;
}
public void operation()
{
System.out.println("叶子业务方法");
}
}
叶子构件只需要覆盖具体业务方法opeartion,对于管理子构件的方法可以提示错误或者抛出异常来处理。
1.2.3 容器构件
class Composite extends Component
{
private ArrayList<Component> list = new ArrayList<>();
public void add(Component c)
{
list.add(c);
}
public void remove(Component c)
{
list.remove(c);
}
public Component getChild(int i)
{
return list.get(i);
}
public void operation()
{
list.forEach(Component::operation);
}
}
容器构件只需要简单实现管理子构件的方法,对于业务方法一般需要对抽象构件集合进行遍历来实现递归调用。
2.组合模式实例
需求:编写程序展示一个学校院系结构:一个学校有多个学院,一个学院有多个系。要求如下图所示:
2.1传统方案解决学校院系
类图
存在的问题分析
- 将学院看做是学校的子类,系是学院的子类,这样实际上是站在组织大小来进行分层次的
- 实际上我们的要求是 :在一个页面中展示出学校的院系组成,一个学校有多个学院,一个学院有多个系,
因此这种方案,不能很好实现的管理的操作,比如对学院、系的添加,删除,遍历等 - 解决方案:把学校、院、系都看做是组织结构,他们之间没有继承的关系,而是一个树形结构,可以更好的实现管理操作。 => 组合模式
2.2组合模式解决学校院系
类图
代码实现
public abstract class OrganizationComponent {
private String name;
private String des;
protected void add(OrganizationComponent organizationComponent){
throw new UnsupportedOperationException();
}
protected void remove(OrganizationComponent organizationComponent){
throw new UnsupportedOperationException();
}
protected abstract void print();
public OrganizationComponent(String name,String des){
this.name=name;
this.des=des;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDes() {
return des;
}
public void setDes(String des) {
this.des = des;
}
}
public class Department extends OrganizationComponent{
public Department(String name, String des) {
super(name, des);
}
@Override
public String getName() {
return super.getName();
}
@Override
public String getDes() {
return super.getDes();
}
@Override
protected void print() {
System.out.println(getName());
}
}
public class College extends OrganizationComponent {
public College(String name, String des) {
super(name, des);
}
List<OrganizationComponent> organizationComponents=new ArrayList<>();
@Override
protected void add(OrganizationComponent organizationComponent) {
organizationComponents.add(organizationComponent);
}
@Override
protected void remove(OrganizationComponent organizationComponent) {
organizationComponents.remove(organizationComponent);
}
@Override
public String getName() {
return super.getName();
}
@Override
public String getDes() {
return super.getDes();
}
@Override
protected void print() {
System.out.println("==========="+getName()+"=================");
for(OrganizationComponent organizationComponent:organizationComponents){
organizationComponent.print();
}
}
}
public class University extends OrganizationComponent {
public University(String name, String des) {
super(name, des);
}
List<OrganizationComponent> organizationComponents=new ArrayList<>();
@Override
protected void add(OrganizationComponent organizationComponent) {
organizationComponents.add(organizationComponent);
}
@Override
protected void remove(OrganizationComponent organizationComponent) {
organizationComponents.remove(organizationComponent);
}
@Override
public String getName() {
return super.getName();
}
@Override
public String getDes() {
return super.getDes();
}
@Override
protected void print() {
System.out.println("==========="+getName()+"=================");
for(OrganizationComponent organizationComponent:organizationComponents){
organizationComponent.print();
}
}
}
public class Client {
public static void main(String[] args) {
//创建学校
OrganizationComponent university=new University("清华大学","中国顶级大学");
//创建学院
OrganizationComponent college1=new College("计算机学院","计算机学院");
OrganizationComponent college2=new College("信息工程学院","信息工程学院");
//创建各个学院下面的系
college1.add(new Department("软件工程","软件工程"));
college1.add(new Department("网络工程","网络工程"));
college1.add(new Department("计算机科学与技术","计算机科学与技术"));
college2.add(new Department("通信工程","通信工程"));
college2.add(new Department("信息工程","信息工程"));
university.add(college1);
university.add(college2);
university.print();
}
}
运行结果
3.透明组合模式与安全组合模式
在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式和安 全组合模式两种形式。
透明组合模式:在抽象构件中声明所有用于管理构件的方法,譬如在示例中 Component 声明了 add、remove 方法,这样做的好处是确保所有的构件类都具有相同的接口,客户端可以针对抽象构件进行统一编程。透明组合模式也是组合模式的标准形式。结构图如下:
透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)
安全组合模式:在抽象构件没有声明管理构件的方法,而是在容器构件中添加管理构件的方法,这种做法是安全的因为叶子对象不可能调用到这些方法。结构图如下:
安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。
4.组合模式在JDK集合的源码分析
Java的集合类-HashMap就使用了组合模式
5.组合模式的注意事项和细节
- 简化客户端操作。客户端只需要面对一致的对象而不用考虑整体部分或者节点叶子的问题。
- 具有较强的扩展性。当我们要更改组合对象时,我们只需要调整内部的层次关系,客户端不用做出任何改动.
- 方便创建出复杂的层次结构。客户端不用理会组合里面的组成细节,容易添加节点或者叶子从而创建出复杂的树形结构
- 需要遍历组织机构,或者处理的对象具有树形结构时, 非常适合使用组合模式.
- 要求较高的抽象性,如果节点和叶子有很多差异性的话,比如很多方法和属性都不一样,不适合使用组合模式
6.优缺点
优点:
- 层次控制:组合模式可以清楚定义分层次的复杂对象,表示对象的全部或者部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制
- 一致使用构件:客户端可以一致地使用容器构件或者叶子构件,也就是能针对构件抽象层一致性编程
- 扩展性好:增加新的容器构件或者叶子构件都很方便,符合开闭原则
- 有效针对树形结构:组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子构件与容器构件的递归组合,可以形成复杂的树形结构,但控制树形结构却很简单
缺点:
- 难以限制构件类型:增加新构件时难以限制构件类型,比如希望容器构件中只有某一特定类型的叶子构件,例如一个只能包含图片的文件夹,使用组合模式时不能依赖类型系统来施加这些约束,需要再运行时进行类型检查来实现,过程较为复杂
7.适用场景
- 具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致性对待它们
- 处理树形结构
- 系统中能够分离出叶子构件以及容器构件,而且类型不固定,需要增加新的叶子构件或者容器构件