组合设计模式
问题场景
假设现在有一个学校,学校中有不同的学院,学院中又有不同的专业,要求设计代码可以方便的对学院。专业进行增加删除,同时还可以遍历出这个学校的所有专业。
在没有学习组合模式之前,我是通过学校类组合学院类,学院类组合专业类来进行设计的。由于学校包含学院,学院包含专业,所以首先需要设计的是专业类,然后是学院类,最后是学校类
// 专业类
public class Department {
public String name;
public Department(String name) {
this.name = name;
}
public void print(String preStr) {
System.out.println(preStr + "-" + name );
}
}
// 学院类
public class College {
public String name;
private List<Department> departments = new ArrayList<>();
public College(String name) {
this.name = name;
}
public void addDepartment(Department department) {
departments.add(department);
}
public void removeDepartment(Department department) {
departments.remove(department);
}
public void print(String preStr) {
System.out.println(preStr + "+" + name );
preStr += " ";
for(Department department : departments) {
department.print(preStr);
}
}
}
// 学校类
public class University {
public String name;
private List<College> colleges;
public University(String name) {
this.name = name;
colleges = new ArrayList<>();
}
public void addCollege(College college) {
colleges.add(college);
}
public void remove(College college) {
colleges.remove(college);
}
public void print(String preStr) {
System.out.println(preStr + "+" + name );
preStr += " ";
for (College college : colleges) {
college.print(preStr);
}
}
}
// 场景类
public class client {
public static void main(String[] args) {
University university = new University("清华大学");
College computerCollege = new College("计算机学院");
College mathCollege = new College("统计学院");
computerCollege.addDepartment(new Department("软件工程"));
computerCollege.addDepartment(new Department("网络工程"));
mathCollege.addDepartment(new Department("应用统计数学"));
mathCollege.addDepartment(new Department("大数据"));
university.addCollege(computerCollege);
university.addCollege(mathCollege);
university.print("");
}
}
/* 控制台输出
+清华大学
+计算机学院
-软件工程
-网络工程
+统计学院
-应用统计数学
-大数据
*/
乍一看完美实现了功能(删除除外,为了减少代码量看的舒服把equals()和hashCode()省略了),但是设计模式就是为了改动而生的,我们现在的组合关系是 学校--------->学院--------->专业,一旦我想在其中增加一环,比方说在学院和专业中间增加个“大方向”,具体叫啥不重要,重要的是这样一来我们不仅要新增一个“大方向”类,还要修改学院中的代码,private List<Department> departments = new ArrayList<>();
中就不能还是Department了,并且,下面的添加删除方法也需要修改,场景类也要进行很大修改,违反开闭原则:对扩展开放**(对提供方),对修改关闭(**对使用方)。再比如说,我想在叶子节点(也就是专业类)下面增加一个类,比方就叫“小方向”,那么专业类就要增加一个List集合,并且增加对应增删方法,无比的麻烦。因此组合设计模式因运而生。
组合模式的概念
组合模式:组合多个对象形成树形结构以表示有整体-部分关系层次结构,组合模式可以让客户端统一对待单个对象和组合对象
组合模式有点难理解!树形结构就是组合模式的体现。文件中可以包含子文件和子文件夹,但是子文件中不能够再继续包含子文件。简单的说子文件就是最后一级别,按照上面的例子,就是学校里面有学院,学院里面有专业。上图好吧,上图靠谱。
从我之前写的代码可以看出,专业类(叶子)不需要提供List集合,也不需要提供增删方法,但学院类学校类(树枝)则如出一辙,正因为树枝类和叶子类的区别对待,导致了维护起来会很复杂(有的树枝变成叶子,有的叶子变成树枝)
所以,组合模式就出现了!通过一些设计。让树枝和叶子作为相同的对象来处理!
在讲组合模式的UML图之前,我先一步一步优化之前的代码推导出组合模式。
首先,由于树枝类都是如出一辙的,树枝类都有增删和遍历的方法,因此我可以定义一个抽象类OrganizationComponent,提供add,remove,print方法,并且,树枝类也需要一个List集合,但是泛型的类是什么呢?如果是OrganizationComponent类型,那学校到学院是可以的(都是树枝),学院到专业呢(树枝到树叶),因此,树枝类和树叶类都必须需要一个父类,这样才可以。那树叶类不是会多出来无法实现的add,remove方法吗,针对这个,我们可以选择报错,也可以在OrganizationComponent类中进行缺省实现。因此,就有了下面组合模式的UML图。
组合模式的UML图
component (抽象构件:容器):它可以是接口或者抽象类,为叶子构建和子容器构建对象声明接口,在该角色中可以包含所有子类共有的行为的实现和声明。在抽象构建中定义了访问及管理它的子构件的方法,如增加子构件,删除子构件,获取子构件等。
leaf(叶子构建):叶子构建可以说就是各种类型的文件!叶子构建没有子构件。它实现了抽象构建中的定义的行为。对于那些访问子容器,删除子容器,增加子容器的就报错。
compsite(子容器构建):它在组合模式中表示容器节点对象,容器结点是子节点,可以是子容器,也可以是叶子构建,它提供一个集合来存储子节点。
更改后:
// OrganizationComponent 容器
public abstract class OrganizationComponent {
public String name;
public OrganizationComponent(String name) {
this.name = name;
}
public void add(OrganizationComponent organizationComponent) {
throw new UnsupportedOperationException();
}
public void remove(OrganizationComponent organizationComponent) {
throw new UnsupportedOperationException();
}
public abstract void print(String preStr);
}
// 树枝类
class Composite extends OrganizationComponent {
// 可能是树枝,也可能是叶子
List<OrganizationComponent> list = new ArrayList<>();
public Composite(String name) {
super(name);
}
@Override
public void add(OrganizationComponent organizationComponent) {
list.add(organizationComponent);
}
@Override
public void remove(OrganizationComponent organizationComponent) {
list.remove(organizationComponent);
}
@Override
public void print(String preStr) {
System.out.println(preStr + "+" + name );
preStr += " ";
for (OrganizationComponent element : list) {
element.print(preStr);
}
}
}
// 叶子类
class Leaf extends OrganizationComponent {
public Leaf(String name) {
super(name);
}
@Override
public void print(String preStr) {
System.out.println(preStr + "-" + name );
}
}
// 客户端调用代码
public class client {
public static void main(String[] args) {
OrganizationComponent university = new Composite("清华大学");
OrganizationComponent computerCollege = new Composite("计算机学院");
OrganizationComponent mathCollege = new Composite("统计学院");
computerCollege.add(new Leaf("软件工程"));
computerCollege.add(new Leaf("网络工程"));
mathCollege.add(new Leaf("应用统计数学"));
mathCollege.add(new Leaf("大数据"));
university.add(computerCollege);
university.add(mathCollege);
university.print();
}
}
/* 控制台输出也和之前一样
+清华大学
+计算机学院
-软件工程
-网络工程
+统计学院
-应用统计数学
-大数据
*/
感受到组合设计模式的好处了吧,完完全全不需要什么学校类学院类专业类,如果还想插入一个“大方向”,那么只需要在客户端调用代码那进行更改即可,对于小方向也是一样。在组合模式种增加新的容器构件和叶子构件都很方便,无需对现有类库进行任何修改,符合开闭原则。并且为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子对象和容器对象的递归组合可以形成复杂的树形机构,但对树形结构的控制却很简单。
比如我增加两个小方向,可以看到,没有任何问题
public class client {
public static void main(String[] args) {
OrganizationComponent university = new Composite("清华大学");
OrganizationComponent computerCollege = new Composite("计算机学院");
OrganizationComponent mathCollege = new Composite("统计学院");
computerCollege.add(new Leaf("软件工程"));
computerCollege.add(new Leaf("网络工程"));
Composite composite = new Composite("应用统计数学");
composite.add(new Leaf("小方向1"));
composite.add(new Leaf("小方向2"));
mathCollege.add(composite);
mathCollege.add(new Leaf("大数据"));
// mathCollege.add(new Leaf("应用统计数学"));
// mathCollege.add(new Leaf("大数据"));
university.add(computerCollege);
university.add(mathCollege);
university.print("");
}
}
/*
+清华大学
+计算机学院
-软件工程
-网络工程
+统计学院
+应用统计数学
-小方向1
-小方向2
-大数据
*/
那如果学院和学校增删的方法里面的逻辑不一样呢?这里我们发现不管是学校还是学院,添加的方法都是list.add(organizationComponent);
缺乏了扩展性,这怎么办呢?,其实我们可以手动做两个子容器class University extends OrganizationComponent
和class College extends OrganizationComponent
,然后在add和remove中填写对应的逻辑,这样子就可以完成扩展,但是这样的话在客户端中就不能new Composite
了,而是new 不同的子容器。
OrganizationComponent university = new University("清华大学");
OrganizationComponent computerCollege = new College("计算机学院");
- 1
- 2
组合模式的安全性和透明性
• 组合模式的安全性是指:从客户使用组合模式上看是否更安全。如果是安全的,那么就不会有发生误操作的可能,能访问的方法都是被支持的功能。
• 组合模式的透明性是指:从客户使用组合模式上看是否需要区分到底是组合对象还是叶子对象。如果是透明的,那就不用再区分,对于客户而言,都是组件对象,具体的类型对于客户而言是透明的,是客户无须关心的。
透明性的实现:
如果把管理子组件的操作定义在Component中,那么客户端只需要面对Component,而无须关心具体的组件类型,这种实现方式就是透明性的实现。前面结构示意代码中就是采用的这一实现方式。
但是透明性的实现是以安全性为代价的,因为在Component中定义的一些方法,对于叶子对象来说是没有意义的,比如增加、删除子组件对象。但这些方法对客户却是透明的,因此客户可能会对叶子对象调用这种增加或删除子组件的方法,这样的操作是不安全的。
组合模式的透明性实现,通常的方式是:在Component中声明管理子组件的操作,并在Component中为这些方法提供默认的实现,对于叶子对象不支持的功能,可以直接抛出一个异常,来表示不支持这个功能。
安全性的实现:
如果把管理子组件的操作定义在Composite中,那么客户端在使用叶子对象的时候,就不会发生使用添加子组件或是删除子组件的操作了,因为压根就没有这样的功能,这种实现方式是安全的。
但是这样一来,客户端在使用的时候,就必须区分到底使用的是Composite对象,还是叶子对象,不同对象的功能是不一样的。
两种实现方式的选择:
对于组合模式而言,在安全性和透明性上,会更看重透明性,毕竟组合模式的功能就是要让用户对叶子对象和组合对象的使用具有一致性。
因此,在使用组合模式的时候,应多采用透明性的实现方式,少用安全性的实现方式。
组合模式的优缺点
优点:
- 统一了组合对象和叶子对象。
- 简化了客户端调用,无须区分操作的是组合对象还是叶子对象。
- 更容易扩展,有了Component的约束,新定义的Composite或Leaf子类能够很容易地与已有的结构一起工作。
缺点:
- 当子组件差别较大的时候,就不太适合用组合模式。
总结
组合模式通过把叶子对象当成特殊的组合对象看待,从而对叶子对象和组合对象一视同仁,全部当成了Component对象,有机地统一了叶子对象和组合对象。
正是因为统一了叶子对象和组合对象,在将对象构建成树形结构的时候,才不需要做区分,反正是组件对象里面包含其他的组件对象,如此递归下去:也才使得对于树形结构的操作变得简单,不管对象类型,统一操作。