组合模式介绍
组合模式(Composite Pattern)也称为部分整体模式(Part-Whole Pattern),是结构型设计模式之一,它将一组相似的对象看做一个对象处理,并根据一个树状结构来组合对象,然后提供统一的方法去访问相应的对象,以此忽略掉对象与对象之间的差别。
组合模式的定义
将对象组合成树形结构以表示 “部分-整体” 的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
组合模式的使用场景
表示部分、整体层次结构时,如树形菜单,文件、文件夹的管理。
组合模式的 UML 类图
角色介绍:
- Component:抽象根节点,定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
- Composite:树枝节点,定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构;
- Leaf:叶子节点,叶子节点对象,其下再无节点,是系统层次遍历的最小单位。
组合模式 在代码具体实现上,有两种不同的方式:
- 透明组合模式:把组合(树节点)使用的方法放到统一行为(Component)中,让不同层次(树节点,叶子节点)的结构都具备一致行为;其 UML 类图如下所示:
把所有公共方法都定义在 Component 中,这样做的好处是客户端无需分辨是叶子节点(Leaf)和树枝节点(Composite),它们具备完全一致的接口;缺点是叶子节点(Leaf)会继承得到一些它所不需要(管理子类操作的方法)的方法,这与设计模式接口隔离原则相违背。 - 安全组合模式:统一行为(Component)只规定系统各个层次的最基础的一致行为,而把组合(树节点)本身的方法(管理子类对象的添加,删除等)放到自身当中;其 UML 类图如下所示:
把系统各层次公有的行为定义在 Component 中,把组合(树节点)特有的行为(管理子类增加,删除等)放到自身(Composite)中。这样做的好处是接口定义职责清晰,符合设计模式 单一职责原则 和 接口隔离原则;缺点是客户需要区分树枝节点(Composite)和叶子节点(Leaf),这样才能正确处理各个层次的操作,客户端无法依赖抽象(Component),违背了设计模式 依赖倒置原则。
组合模式的实现
这里以文件管理器中的文件和文件夹的层级结构为例
透明组合模式
抽象根节点 Component
public abstract class Dir {
private String name;
public Dir(String name) {
this.name = name;
}
public abstract void addDir(Dir dir);
public abstract void removeDir(Dir dir);
// 打印目录结构
public abstract void print(int depth);
public String getName() {
return name;
}
}
树枝节点 Composite
对应的就是文件夹
public class Folder extends Dir {
private List dirs = new ArrayList<>();
public Folder(String name) {
super(name);
}
@Override
public void addDir(Dir dir) {
dirs.add(dir);
}
@Override
public void removeDir(Dir dir) {
dirs.remove(dir);
}
@Override
public void print(int depth) {
for (int i = 0; i < depth; i++) {
System.out.print("--");
}
System.out.println(getName());
for (Dir dir : dirs) {
dir.print(depth + 1);
}
}
}
叶子节点 Leaf
也就是文件类
public class File extends Dir {
public File(String name) {
super(name);
}
@Override
public void addDir(Dir dir) {
throw new UnsupportedOperationException("文件对象不支持该操作");
}
@Override
public void removeDir(Dir dir) {
throw new UnsupportedOperationException("文件对象不支持该操作");
}
@Override
public void print(int depth) {
for (int i = 0; i < depth; i++) {
System.out.print("--");
}
System.out.println(getName());
}
}
文件类不包含添加和删除的操作,故抛出异常,这里就违背了接口隔离原则。
客户端
public class Client {
public static void main(String[] args) {
Dir root = new Folder("/");
Dir file = new File("root.txt");
root.addDir(file);
root.removeDir(file);
Dir folder1 = new Folder("home");
Dir file1 = new File("home.txt");
folder1.addDir(file1);
Dir folder2 = new Folder("etc");
Dir file2 = new File("etc.conf");
folder2.addDir(file2);
root.addDir(folder1);
root.addDir(folder2);
root.print(0);
}
}
客户端声明类型均采用抽象类型,符合依赖倒置原则。
输出结果:
/
--home
----home.txt
--etc
----etc.conf
安全组合模式
将添加,删除操作只存在树枝节点中,就变为安全组合模式。叶子节点就无需重写自己不需要的方法,符合接口隔离原则,此时客户端要创建树枝节点,只能声明为 Folder 类型,违背了依赖导致原则。
问:透明组合模式 和 安全组合模式 都有各自的优点和缺点,那么我们应该优先选择哪一种呢?
答:既然组合模式会被分为两种实现,那么肯定是不同的场合某一种会更加适合,也即具体情况具体分析。透明组合模式 将公共接口封装到抽象根节点(Component)中,那么系统所有节点就具备一致行为,所以如果当系统绝大多数层次具备相同的公共行为时,采用 透明组合模式 也许会更好(代价:为剩下少数层次节点引入不需要的方法);而如果当系统各个层次差异性行为较多或者树节点层次相对稳定(健壮)时,采用 安全组合模式。
总结
优点
1.可以清晰地定义分层次的复杂对象。
2.增加节点方便。
缺点
1.透明组合模式 和 安全组合模式,各自优缺点比较明显,需要根据实际情况进行选择。
Android 源码中组合模式
Android 源码中有一个非常经典的组合模式实现,那就是 View 和 ViewGroup 的组合,如下图。
ViewGroup 就是容器,相当于树枝节点,其可以包含 TextView,Button等,也可以包含继承自 ViewGroup 的节点,但对于叶子节点 TextView 等就无法包含 ViewGroup。将添加、移除子节点的操作都定义在了 ViewGroup 中,故该组合模式为安全的组合模式。
为什么 ViewGroup 有容器的功能
ViewGroup 是继承自 View 类的。
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
...
}
ViewGroup 与 View 差别就在于 ViewGroup 实现了 ViewParent 和 ViewManager接口。
ViewManager 定义了 addView、removeView 等对子视图操作的方法
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
而 ViewParent 则定义了刷新容器的接口 requestLayout 和其他一些焦点事件处理的接口
public interface ViewParent {
// 请求重新布局
public void requestLayout();
// 获取父 View(不是父类)
public ViewParent getParent();
// 请求子视图的焦点
public void requestChildFocus(View child, View focused);
...
}
ViewGroup 实现的这两个接口与 View 不一样,还有重要一点就是 ViewGroup 是抽象类,其将 View 中的 onLayout 方法重置为抽象方法,也就是容器子类必须实现,View 中该方法是空实现,因为对应一个普通 View 来说该方法没有什么实现价值。View 测绘流程的 onMeasure 和 onDraw 在 ViewGroup 都没有重写,对于 onMeasure 方法,在ViewGroup 中增加了测量子View的方法,如 measureChildren;而对于 onDraw 方法,ViewGroup 中定义了 dispatchDraw 方法来调用每一个子 View 的 onDraw 方法。由此可见,ViewGroup 就是一个容器,只负责对子元素的操作,而非具体的个体行为。