0%

设计模式之结构型模式(三):组合模式

1. 组合模式概述

  • 概念

    • 又叫整体模式,是用于把一组相似的对象当作一个单一的对象。虽然整体包含了部分,但无论整体或部分,都具有一致的行为
    • 组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构
  • 特点

    • 桥接模式用于将同等级的接口互相组合;组合模式用于整体与部分的结构
    • 当整体与部分有相似的结构,在操作时可以被一致对待时,就可以使用组合模式。例如
      • 大树枝和分树枝的关系:树枝可以长出叶子、也可以长出树枝,分树枝也一样
      • 总公司和子公司的关系:总公司可以设立部门、也可以设立分公司,子公司也一样
      • 文件夹和子文件夹的关系:文件夹中可以存放文件、也可以新建文件夹,子文件夹也一样

2. 组合模式 Demo

组合模式

  1. 不使用组合模式的设计方案

    • 新建管理者类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      public class Manager {
      // 职位
      private String position;
      // 工作内容
      private String job;
      // 管理的管理者
      private List<Manager> managers = new ArrayList<>();
      // 管理的职员
      private List<Employee> employees = new ArrayList<>();

      public Manager(String position, String job) {
      this.position = position;
      this.job = job;
      }

      public void addManager(Manager manager) {
      managers.add(manager);
      }

      public void removeManager(Manager manager) {
      managers.remove(manager);
      }

      public void addEmployee(Employee employee) {
      employees.add(employee);
      }

      public void removeEmployee(Employee employee) {
      employees.remove(employee);
      }

      // 做自己的本职工作
      public void work() {
      System.out.println("我是" + position + ",我正在" + job);
      }

      // 检查下属
      public void check() {
      work();
      for (Employee employee : employees) {
      employee.work();
      }
      for (Manager manager : managers) {
      manager.check();
      }
      }
      }
    • 新建职员类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      public class Employee {
      // 职位
      private String position;
      // 工作内容
      private String job;

      public Employee(String position, String job) {
      this.position = position;
      this.job = job;
      }

      // 做自己的本职工作
      public void work() {
      System.out.println("我是" + position + ",我正在" + job);
      }
      }
    • 客户端建立人员结构关系

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      public class Client {

      @Test
      public void test() {
      Manager boss = new Manager("老板", "唱怒放的生命");
      Employee HR = new Employee("人力资源", "聊微信");
      Manager PM = new Manager("产品经理", "不知道干啥");
      Manager CFO = new Manager("财务主管", "看剧");
      Manager CTO = new Manager("技术主管", "划水");
      Employee UI = new Employee("设计师", "画画");
      Employee operator = new Employee("运营人员", "兼职客服");
      Employee webProgrammer = new Employee("程序员", "学习设计模式");
      Employee backgroundProgrammer = new Employee("后台程序员", "CRUD");
      Employee accountant = new Employee("会计", "背九九乘法表");
      Employee clerk = new Employee("文员", "给老板递麦克风");
      boss.addEmployee(HR);
      boss.addManager(PM);
      boss.addManager(CFO);
      PM.addEmployee(UI);
      PM.addManager(CTO);
      PM.addEmployee(operator);
      CTO.addEmployee(webProgrammer);
      CTO.addEmployee(backgroundProgrammer);
      CFO.addEmployee(accountant);
      CFO.addEmployee(clerk);

      boss.check();
      }
      }
    • 运行测试方法,输出如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      我是老板,我正在唱怒放的生命
      我是人力资源,我正在聊微信
      我是产品经理,我正在不知道干啥
      我是设计师,我正在画画
      我是运营人员,我正在兼职客服
      我是技术主管,我正在划水
      我是程序员,我正在学习设计模式
      我是后台程序员,我正在CRUD
      我是财务主管,我正在看剧
      我是会计,我正在背九九乘法表
      我是文员,我正在给老板递麦克风
    • 这样设计的公司结构有两个弊端:name 字段、job 字段、work 方法重复了,可以将重复的字段和方法提取到一个工具类中,这样可以消除重复,但逻辑上并不利于程序的高内聚;管理者对其管理的管理者和职员需要区别对待,此方案无法解决,此方案中 Employee 和 Manager 类完全是两个不同的对象,两者的相似性被忽略了

  2. 透明方式的组合模式

    • 新建一个抽象的组件类(组合模式最主要的功能就是让用户可以一致对待整体和部分结构,将两者都作为一个相同的组件)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      public abstract class Component {
      // 职位
      private String position;
      // 工作内容
      private String job;

      public Component(String position, String job) {
      this.position = position;
      this.job = job;
      }

      // 做自己的本职工作
      public void work() {
      System.out.println("我是" + position + ",我正在" + job);
      }

      abstract void addComponent(Component component);

      abstract void removeComponent(Component component);

      abstract void check();
      }
    • 管理者继承自该抽象类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      public class Manager extends Component {
      // 管理的组件
      private List<Component> components = new ArrayList<>();

      public Manager(String position, String job) {
      super(position, job);
      }

      @Override
      public void addComponent(Component component) {
      components.add(component);
      }

      @Override
      void removeComponent(Component component) {
      components.remove(component);
      }

      // 检查下属
      @Override
      public void check() {
      work();
      for (Component component : components) {
      component.check();
      }
      }
      }
    • 职员同样继承该抽象类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      public class Employee extends Component {

      public Employee(String position, String job) {
      super(position, job);
      }

      @Override
      void addComponent(Component component) {
      System.out.println("职员没有管理权限");
      }

      @Override
      void removeComponent(Component component) {
      System.out.println("职员没有管理权限");
      }

      @Override
      void check() {
      work();
      }
      }
    • 修改客户端如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      public class Client {

      @Test
      public void test(){
      Component boss = new Manager("老板", "唱怒放的生命");
      Component HR = new Employee("人力资源", "聊微信");
      Component PM = new Manager("产品经理", "不知道干啥");
      Component CFO = new Manager("财务主管", "看剧");
      Component CTO = new Manager("技术主管", "划水");
      Component UI = new Employee("设计师", "画画");
      Component operator = new Employee("运营人员", "兼职客服");
      Component webProgrammer = new Employee("程序员", "学习设计模式");
      Component backgroundProgrammer = new Employee("后台程序员", "CRUD");
      Component accountant = new Employee("会计", "背九九乘法表");
      Component clerk = new Employee("文员", "给老板递麦克风");
      boss.addComponent(HR);
      boss.addComponent(PM);
      boss.addComponent(CFO);
      PM.addComponent(UI);
      PM.addComponent(CTO);
      PM.addComponent(operator);
      CTO.addComponent(webProgrammer);
      CTO.addComponent(backgroundProgrammer);
      CFO.addComponent(accountant);
      CFO.addComponent(clerk);

      boss.check();
      }
      }
    • 分析

      • 使用组合模式,解决了之前的两个弊端,一是将共有的字段与方法移到了父类中、消除了重复;二是在客户端中,可以一致对待 Manager 和 Employee 类。即:Manager 类和 Employee 类统一声明为 Component 对象;统一调用 Component 对象的 addComponent() 方法添加子对象即可
      • Employee 类虽然继承了父类的 addComponent() 方法和 removeComponent() 方法,但仅仅提供了一个空实现,因为 Employee 类是不支持添加和移除组件的。违背了接口隔离原则:客户端不应依赖它不需要的接口,如果一个接口在实现时,部分方法由于冗余被客户端空实现,则应该将接口拆分,让实现类只需依赖自己需要的接口方法
      • 这种违背了接口隔离原则的组合模式称为透明方式:在 Component 中声明所有管理子对象的方法,包括 add()remove() 等,这样继承自 Component 的子类都具备了 add()remove() 方法。对于外界来说叶节点和枝节点是透明的,它们具备完全一致的接口
        • 透明方式组合模式的优点:让 Manager 类和 Employee 类具备完全一致的行为接口,调用者可以一致对待它们
        • 透明方式组合模式的缺点:Employee 类并不支持管理子对象,不仅违背了接口隔离原则,而且客户端可以用 Employee 类调用 addComponent()removeComponent() 方法,导致程序出错,所以这种方法是不安全的
  1. 安全方式的组合模式

    • 修改抽象类:在父类中去掉了 addComponent()removeComponent() 这两个抽象方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      public abstract class Component {
      // 职位
      private String position;
      // 工作内容
      private String job;

      public Component(String position, String job) {
      this.position = position;
      this.job = job;
      }

      // 做自己的本职工作
      public void work() {
      System.out.println("我是" + position + ",我正在" + job);
      }

      abstract void check();
      }
    • 修改 Manager 类:Manager 单独实现了 addComponent()removeComponent() 这两个方法,去掉了 @Override 注释

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      public class Manager extends Component {
      // 管理的组件
      private List<Component> components = new ArrayList<>();

      public Manager(String position, String job) {
      super(position, job);
      }

      public void addComponent(Component component) {
      components.add(component);
      }

      void removeComponent(Component component) {
      components.remove(component);
      }

      // 检查下属
      @Override
      public void check() {
      work();
      for (Component component : components) {
      component.check();
      }
      }
      }
    • 修改 Employee 类:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public class Employee extends Component {

      public Employee(String position, String job) {
      super(position, job);
      }

      @Override
      void check() {
      work();
      }
      }
    • 客户端建立人员结构关系

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      public class Client {

      @Test
      public void test(){
      Manager boss = new Manager("老板", "唱怒放的生命");
      Employee HR = new Employee("人力资源", "聊微信");
      Manager PM = new Manager("产品经理", "不知道干啥");
      Manager CFO = new Manager("财务主管", "看剧");
      Manager CTO = new Manager("技术主管", "划水");
      Employee UI = new Employee("设计师", "画画");
      Employee operator = new Employee("运营人员", "兼职客服");
      Employee webProgrammer = new Employee("程序员", "学习设计模式");
      Employee backgroundProgrammer = new Employee("后台程序员", "CRUD");
      Employee accountant = new Employee("会计", "背九九乘法表");
      Employee clerk = new Employee("文员", "给老板递麦克风");
      boss.addComponent(HR);
      boss.addComponent(PM);
      boss.addComponent(CFO);
      PM.addComponent(UI);
      PM.addComponent(CTO);
      PM.addComponent(operator);
      CTO.addComponent(webProgrammer);
      CTO.addComponent(backgroundProgrammer);
      CFO.addComponent(accountant);
      CFO.addComponent(clerk);

      boss.check();
      }
      }
    • 分析

      • 安全方式:在 Component 中不声明 add 和 remove 等管理子对象的方法,这样叶节点就无需实现它,只需在枝节点中实现管理子对象的方法即可

        • 安全方式的组合模式优点:安全方式遵循了接口隔离原则
        • 安全方式的组合模式缺点:安全方式由于不够透明,Manager 和 Employee 不具有相同的接口,客户端无法将 Manager 和 Employee 统一声明为 Component,必须要区别对待,带来了使用上的不方便
      • 透明方式和安全方式各有好处,在使用组合模式时,需要根据实际情况决定。大多数使用组合模式的场景都是采用透明方式,虽然有点不安全,但是客户端无需做任何判断来区分是叶子结点还是枝节点,这一点很方便

  2. 组合模式中的透明方式与安全方式的区别

    • 透明方式:在 Component 中声明所有管理子对象的方法,包括 add、remove 等,这样继承自 Component 的子类都具备了 add、remove 方法。对于外界来说叶节点和枝节点是透明的,它们具备完全一致的接口
    • 安全方式:在 Component 中不声明 add 和 remove 等管理子对象的方法,这样叶节点就无需实现它,只需在枝节点中实现管理子对象的方法即可
-------------------- 本文结束感谢您的阅读 --------------------