组合模式
计算机的文件系统中,一个文件夹中既可以放入文件也可以放入其他文件夹(子文件夹),所以文件夹和文件虽然是不同类型的对象,但它们都是可以被放入到文件夹中,可以统称为"目录条目"(directory entry)
在目录条目中,我们把文件夹和文件当作同一对象看待。
将容器和内容作为同一种东西看待,在容器中既可以放入内容,也可以放入小容器,然后在那个小容器中,又可以继续放入更小的容器,这样就形成一种容器的递归结构(有点类似于俄罗斯套娃的感觉)
在 GoF 的《设计模式》⼀书中,组合模式是这样定义的:
Compose objects into tree structure to represent part-whole hierarchies.Composite lets client treat individual objects and compositions of objects uniformly.
Composite模式就是创造出使容器与内容具有一致性的递归结构的模式。它将对象组合成树型结构以表示"部分-整体"的层次结构,使得用户对单个对象和组合对象的操作具有一致性,主要是⽤来处理树形结构数据,属于结构型模式。
组合模式模板
组合模式通常包含三种角色 :
- Component:抽象根节点,为Composite和Leaf声明接口,是所有节点的上层父类,可以为一些功能方法定义缺省的实现
- Leaf 叶子节点, 其下不再包含其他的子节点对象
- Composite 树枝节点 通常会包含子节点,组合的树枝节点和子节点形成一个树形结构
其模板如下:
public abstract class Component {
protected abstract void doSomeOperation();
/**
* 向组合对象中加入组件对象
* 叶子对象没有该功能
*
* */
protected void addChild(Component component) {
throw new UnsupportedOperationException("此对象不支持addChild该功能");
}
/**
* 向组合对象中移除组件对象
* 叶子对象没有该功能
*
* */
protected void removeChild(Component component){
throw new UnsupportedOperationException("此对象不支持removeChild该功能");
}
}
public class Leaf extends Component {
protected void doSomeOperation() {
//TODO
}
}
public class Composite extends Component {
private List<Component> componentList = null;
protected void doSomeOperation() {
//TODO
}
@Override
protected void addChild(Component component) {
if(componentList == null){
componentList = new ArrayList<Component>();
}
componentList.add(component);
}
@Override
protected void removeChild(Component component) {
if(componentList != null){
componentList.remove(component);
}
}
}
这是组合模式的模板类图:树枝节点/子节点的方法可以根据实际情况来定义
透明组合模式代码示例
在一个商品管理的系统里,我们需要打印一个商品树的树型结构,如下图:
可以将这里的节点分成两类:一种是没有子节点的叶子节点:夹克,衬衫等 ; 一种是有子节点的树枝节点:服务,女装等。
我们为其定义一个抽象父类,这样叶子节点和容器节点既能保证一致性,又可以有各自不同的实现
public abstract class Component {
private String name = "";
public Component(String name){
this.name = name;
}
protected String getName(){
return name;
}
protected abstract void printStruct(String prefix);
/**
* 向组合对象中加入组件对象
* 叶子对象没有该功能
*
* */
protected void addChild(Component component) {
throw new UnsupportedOperationException("此对象不支持addChild该功能");
}
/**
* 向组合对象中移除组件对象
* 叶子对象没有该功能
*
* */
protected void removeChild(Component component){
throw new UnsupportedOperationException("此对象不支持removeChild该功能");
}
}
public class Leaf extends Component {
public Leaf(String name){
super(name);
}
/***
* 打印对应的叶子结构
* @param prefix 前缀 实现向后缩进
*
*/
public void printStruct(String prefix){
System.out.println(prefix + "-" + super.getName());
}
}
public class Composite extends Component{
List<Component> componentList = new ArrayList<>();
public Composite(String name) {
super(name);
}
@Override
protected void printStruct(String prefix) {
System.out.println(prefix + "+" + getName());
if (componentList != null) {
//打印其叶子节点,缩进一个空格
for(Component component: componentList) {
component.printStruct(prefix + prefix);
}
}
}
@Override
protected void addChild(Component component) {
this.componentList.add(component);
}
@Override
protected void removeChild(Component component) {
this.componentList.remove(component);
}
}
public class Client {
public static void main(String[] args) {
//组合对象 作为根节点
Component root = new Composite("服装");
//组合对象
Component composite1 = new Composite("夏装");
Component composite2 = new Composite("冬装");
//叶子节点
Leaf leaf1 = new Leaf("裙子");
Leaf leaf2 = new Leaf("背心");
Leaf leaf3 = new Leaf("棉衣");
root.addChild(composite1);
root.addChild(composite2);
composite1.addChild(leaf1);
composite1.addChild(leaf2);
composite2.addChild(leaf3);
root.printStruct("");
}
}
输出结果:
组合模式的安全性和透明性
组合模式中,Composite对象就像是一个容器,可以包含其他的树枝节点或者叶子节点。作为一个容器, 我们可以向其中添加或者删除子节点(包括Leaf和Composite节点)
这样在使用组合模式的时候就需要考虑对叶子节点的操作是在Component还是Composite中声明?这就是对安全性和透明性之前的权衡。
安全性 : 从用户使用角度上组合模式是否安全,如果是安全的,那么用户在使用的时候就不会有发生误操作的可能性,能访问的都是支持的有意义的操作
透明性 : 用户使用组件对象的时候是否需要区分到底是容器对象还是叶子对象, 如果是透明的,就不需要区分 (上面的写法其实就是透明组合模式的写法)
透明性的实现:将操作子节点的方法定义在Component, 同时为了兼顾安全性,可以在Component中为这些方法提供默认实现 比方说抛出异常,默认的空实现或者定义成抽象方法。不过这也违背了设计的单一原则
安全性的实现: 把操作子节点的方法定义在Composite
透明组合模式将所有的接口都定义在抽象根节点里,当叶子节点和树枝节点绝大多数情形下都具备相同的行为时,使用透明组合模式更加方便;而如果不同层次的节点的行为差异较大,更推荐安全组合模式的写法。
组合模式在源码中的应用
HashMap里就使用到了组合模式
public static void main(String[] args) {
Map<String, String> root = new HashMap<>();
map.put("0", "Xiaoming");
Map<String, String> map = new HashMap<>();
map.put("1", "Tom");
map.put("2", "Alice");
root.putAll(map);
}
这里的Map是一个抽象根节点,HashMap是一个树枝节点,Node是叶子节点
HashMap中存储的就是一个静态内部类的数组 bashNode<K,V>[] tab
ArrayList也类似,Collection是其抽象根节点,ArrayList是树枝节点,叶子节点是 Object
总结
组合模式,将⼀组对象组织成树形结构,将单个对象和组合对象都看做树中的节点,以统⼀处理逻辑,并且它利⽤树形结构的特点,递归地处理每个⼦树,依次简化代码实现。使⽤组合模式的前提在于,你的业务场景必须能够表⽰成树形结构。
优点
- 定义了基本对象和组合对象的类层次结构
- 统一了组合对象和叶子对象,简化了客户端的调用
缺点 : 很难限制组合中的节点类型