设计模式-组合模式

组合模式介绍

组合模式(Composite Pattern)也称为部分整体模式(Part-Whole Pattern),是结构型设计模式之一,它将一组相似的对象看做一个对象处理,并根据一个树状结构来组合对象,然后提供统一的方法去访问相应的对象,以此忽略掉对象与对象之间的差别。

组合模式的定义

将对象组合成树形结构以表示 “部分-整体” 的层次结构,使得用户对单个对象和组合对象的使用具有一致性。

组合模式的使用场景

表示部分、整体层次结构时,如树形菜单,文件、文件夹的管理。

组合模式的 UML 类图

角色介绍:

  • Component:抽象根节点,定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
  • Composite:树枝节点,定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构;
  • Leaf:叶子节点,叶子节点对象,其下再无节点,是系统层次遍历的最小单位。

组合模式 在代码具体实现上,有两种不同的方式:

  1. 透明组合模式:把组合(树节点)使用的方法放到统一行为(Component)中,让不同层次(树节点,叶子节点)的结构都具备一致行为;其 UML 类图如下所示:
    设计模式-组合模式_第1张图片
    透明组合模式

    把所有公共方法都定义在 Component 中,这样做的好处是客户端无需分辨是叶子节点(Leaf)和树枝节点(Composite),它们具备完全一致的接口;缺点是叶子节点(Leaf)会继承得到一些它所不需要(管理子类操作的方法)的方法,这与设计模式接口隔离原则相违背。
  2. 安全组合模式:统一行为(Component)只规定系统各个层次的最基础的一致行为,而把组合(树节点)本身的方法(管理子类对象的添加,删除等)放到自身当中;其 UML 类图如下所示:
    设计模式-组合模式_第2张图片
    安全组合模式

    把系统各层次公有的行为定义在 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 源码中有一个非常经典的组合模式实现,那就是 ViewViewGroup 的组合,如下图。

设计模式-组合模式_第3张图片

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 测绘流程的 onMeasureonDraw 在 ViewGroup 都没有重写,对于 onMeasure 方法,在ViewGroup 中增加了测量子View的方法,如 measureChildren;而对于 onDraw 方法,ViewGroup 中定义了 dispatchDraw 方法来调用每一个子 View 的 onDraw 方法。由此可见,ViewGroup 就是一个容器,只负责对子元素的操作,而非具体的个体行为。

你可能感兴趣的:(设计模式-组合模式)