一、组合模式定义
Compose objects into tree structures to represent part-whole hierarchies.Composite lets clients treat individual objects and compositions of objects uniformly.
(将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。)
二、组合模式的结构和说明
- Component 抽象的组件对象,为参与组合的对象声明接口,让客户端可以通过这个接口来访问和管理整个对象结构,可以定义一些默认的行为或属性。
- Leaf 叶子节点对象,其下不再包含其它的子节点,也就是遍历的最小单位。
- Composite 组合对象,通常会存储子组件,形成一个树形结构。
注意:
- 这里透明性是指:从客户使用组合模式时,是否需要区分是组合对象还是叶子对象。如果是透明的,那就不需要区分,都是组件对象,跟客户无关。
- 这里安全性是指:从客户使用组合模式上看是否更安全。能访问的方法都是支持的,就说明是安全的。如果可能会发生误操作,则是不安全的。
三、组合模式示例
如下,我们的商品系统中有一个这样的商品类别树,现在需要管理商品类别树并输出它的结构,应该怎么实现呢?
+服装
+女装
-裙子
-旗袍
+男装
-衬衣
-夹克
不用模式的解决方案
观察上边的商品类别树,上边的节点可以分为两类:1、可以包含其它节点的节点,称为容器节点;2、没有子节点的节点,称为叶子节点。
那我们就可以分别定义一个容器类和一个叶子节点类。
容器类,也就是组合类。容器类里边可以包含其它容器类或者叶子节点类,由于容器类和叶子类类型不同,所以需要分开保存。
/**
* 组合对象,可以包含其它组合对象或者叶子对象
*/
@Data
public class Composite {
/**
* 用来记录包含的其它组合对象
*/
private Collection<Composite> childComposite = new ArrayList<>();
/**
* 用来记录包含的其它叶子对象
*/
private Collection<Leaf> childLeaf = new ArrayList<>();
/**
* 组合对象的名字
*/
private String name;
public Composite(String name){
this.name = name;
}
/**
* 向组合对象加入被它包含的其它组合对象
* @param c 被它包含的其它组合对象
*/
public void addComposite(Composite c){
this.childComposite.add(c);
}
/**
* 向组合对象加入被它包含的叶子对象
* @param leaf 被它包含的叶子对象
*/
public void addLeaf(Leaf leaf){
this.childLeaf.add(leaf);
}
/**
* 输出组合对象自身的结构
* @param preStr 前缀,主要是按照层级拼接的空格,实现向后缩进
*/
public void printStruct(String preStr){
//先把自己输出去
System.out.println(preStr+"+"+this.name);
//然后添加俩空格,表示向后缩进俩空格,输出自己包含的叶子对象
preStr+=" ";
// 输出所有的叶子节点
for(Leaf leaf : childLeaf){
leaf.printStruct(preStr);
}
//输出当前对象的子组合对象
for(Composite c : childComposite){
//递归输出每个子组合对象
c.printStruct(preStr);
}
}
}
叶子节点类:
/**
* 叶子对象
*/
@Data
public class Leaf {
/**
* 叶子对象的名字
*/
private String name;
public Leaf(String name){
this.name = name;
}
/**
* 输出叶子对象的结构,叶子对象没有子对象,也就是输出叶子对象的名字
* @param preStr 前缀,主要是按照层级拼接的空格,实现向后缩进
*/
public void printStruct(String preStr){
System.out.println(preStr+"-"+name);
}
}
我们通过客户端来测试一下:
public static void main(String[] args) {
//定义所有的组合对象
Composite root = new Composite("服装");
Composite c1 = new Composite("男装");
Composite c2 = new Composite("女装");
//定义所有的叶子对象
Leaf leaf1 = new Leaf("衬衣");
Leaf leaf2 = new Leaf("夹克");
Leaf leaf3 = new Leaf("裙子");
Leaf leaf4 = new Leaf("套装");
//按照树的结构来组合组合对象和叶子对象
root.addComposite(c1);
root.addComposite(c2);
c1.addLeaf(leaf1);
c1.addLeaf(leaf2);
c2.addLeaf(leaf3);
c2.addLeaf(leaf4);
//调用根对象的输出功能来输出整棵树
root.printStruct("");
}
运行后,输出结果如下:
+服装
+女装
-裙子
-旗袍
+男装
-衬衣
-夹克
这样是可以实现需要的功能的,但是存在一个明显的问题:组合对象和叶子对象必须要区别对待,在组合对象中需要将其分开保存,在客户端中使用时,也要分开创建。这样不仅让程序变的复杂,也对功能的扩展带来不便。
使用组合模式来重构
下面我们来看下怎么用组合模式来实现需要的功能。
组合模式通过引入一个抽象的组件对象,作为组合对象和叶子对象的父对象,这样就把组合对象和叶子对象统一起来了,在使用的时候,直接操作组件对象,而不需要区分操作的是组合对象还是叶子对象。
透明性组合模式:
定义抽象组件
/**
* 抽象的组件对象,为组合中的对象声明接口,实现接口的缺省行为
*/
@Data
public abstract class Component {
/**
* 输出组件自身的名称
*/
public abstract void printStruct(String preStr);
/**
* 向组合对象中加入组件对象
* @param child 被加入组合对象中的组件对象
*/
public void addChild(Component child) {
// 缺省的实现,抛出例外,因为叶子对象没有这个功能,或者子组件没有实现这个功能
throw new UnsupportedOperationException("对象不支持这个功能");
}
}
组合类,继承抽象组件类。可以保存其它组合对象或者叶子对象,不需要分开保存
/**
* 组合对象,可以包含其它组合对象或者叶子对象
*/
@Data
public class Composite extends Component{
/**
* 用来存储组合对象中包含的子组件对象
*/
private List<Component> childComponents = new ArrayList<>();
/**
* 组合对象的名字
*/
private String name;
public Composite(String name){
this.name = name;
}
/**
* 向组合对象加入被它包含的其它组合对象
* @param c 被它包含的其它组合对象
*/
@Override
public void addChild(Component c){
this.childComponents.add(c);
}
/**
* 输出组合对象自身的结构
* @param preStr 前缀,主要是按照层级拼接的空格,实现向后缩进
*/
@Override
public void printStruct(String preStr){
//先把自己输出去
System.out.println(preStr+"+"+this.name);
//然后添加俩空格,表示向后缩进俩空格,输出自己包含的叶子对象
preStr+=" ";
//输出当前对象的子组合对象
for(Component c : childComponents){
//递归输出每个子组合对象
c.printStruct(preStr);
}
}
}
叶子节点类,也继承抽象组件类
/**
* 叶子对象
*/
@Data
public class Leaf extends Component{
/**
* 叶子对象的名字
*/
private String name;
public Leaf(String name){
this.name = name;
}
/**
* 输出叶子对象的结构,叶子对象没有子对象,也就是输出叶子对象的名字
* @param preStr 前缀,主要是按照层级拼接的空格,实现向后缩进
*/
@Override
public void printStruct(String preStr){
System.out.println(preStr+"-"+name);
}
}
我们通过客户端来测试一下:
public static void main(String[] args) {
//定义所有的组合对象
Component root = new Composite("服装");
Component c1 = new Composite("女装");
Component c2 = new Composite("男装");
//定义所有的叶子对象
Component leaf1 = new Leaf("裙子");
Component leaf2 = new Leaf("旗袍");
Component leaf3 = new Leaf("衬衣");
Component leaf4 = new Leaf("夹克");
//按照树的结构来组合组合对象和叶子对象
root.addChild(c1);
root.addChild(c2);
c1.addChild(leaf1);
c1.addChild(leaf2);
c2.addChild(leaf3);
c2.addChild(leaf4);
// 打印输出整棵树结构
root.printStruct("");
}
运行完后,会得到同样的输出。
这样一来,在组合对象中保存时不需要将组合对象和叶子节点对象分开保存,并且在客户端中使用时,也不需要区分操作,都操作组件对象就可以了。
安全性组合模式:
上边实现的方式是透明性方式,其实安全性方式跟透明性方式区别很小,透明性组合模式把操作子组件的方法定义在了Component中,这样客户端只需要面对Component,而不需要关心子组件的具体类型。这种方式是以安全性为代价的,假如客户在操作的时候,不清楚叶子节点中没有重写addChild方法,那么通过叶子节点调用了这个方法,就会抛出异常。
想要将上边透明性组合模式改为安全性组合模式,只需要在两处做简单修改即可。
第一个地方,去掉抽象组件类中的addChild方法(Composite组合类中的方法去掉@Override注解)
/**
* 抽象的组件对象,为组合中的对象声明接口,实现接口的缺省行为
*/
public abstract class Component {
/**
* 输出组件自身的名称
*/
public abstract void printStruct(String preStr);
}
第二个地方,在客户端创建使用时,还是需要区分创建组合类和叶子节点类
//定义所有的组合对象
Composite root = new Composite("服装");
Composite c1 = new Composite("女装");
Composite c2 = new Composite("男装");
//定义所有的叶子对象
Leaf leaf1 = new Leaf("裙子");
Leaf leaf2 = new Leaf("旗袍");
Leaf leaf3 = new Leaf("衬衣");
Leaf leaf4 = new Leaf("夹克");
//按照树的结构来组合组合对象和叶子对象
root.addChild(c1);
root.addChild(c2);
c1.addChild(leaf1);
c1.addChild(leaf2);
c2.addChild(leaf3);
c2.addChild(leaf4);
// 打印输出整棵树结构
root.printStruct("");
运行一下,会得到同样的输出。
透明性模式和安全性模式的选择
对于组合模式而言,在选择上会更倾向于透明性。毕竟组合模式的初衷就是要让用户对组合对象和叶子对象的使用具有一致性。
四、组合模式的优缺点
优点:
- 统一了组合对象和叶子节点对象。
- 简化了客户端调用。 客户端在使用的时候,不需要区分各个组件。
- 更容易扩展。 新增一个Component节点的子类时,很容易与已有结构一起工作,客户端不需要跟着做改变。
缺点:
- 很难限制组合中的组件类型。 在需要检测组件类型的时候,无法依靠编译器的类型约束来完成,必须在运行期间动态检测。
五、组合模式的应用场景及案例
- 维护和展示"部分-整体"结构的场景,如树形菜单、文件目录、组织架构等。
六、注意事项
组合模式中,如果不是特殊设计,一定要避免出现环状引用的情形。如A包含B,B包含C,C包含A,这样就构成了一个环状引用。环状引用容易出现死循环,或者导致逻辑被重复执行多次的情形。
检测环状引用,可以通过记录和拼接路径检测,亦或是其它方式,这里就不做拓展了。感兴趣的小伙伴可以自己了解下。