【Java设计模式 设计模式与范式】结构型模式 六:组合模式

简介: 【Java设计模式 设计模式与范式】结构型模式 六:组合模式

本篇Blog继续学习结构型模式,了解如何更优雅的布局类和对象。结构型模式描述如何将类或对象按某种布局组合以便获得更好、更灵活的结构。虽然面向对象的继承机制提供了最基本的子类扩展父类的功能,但结构型模式不仅仅简单地使用继承,而更多地通过组合与运行期的动态组合来实现更灵活的功能。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。本篇学习的是组合模式。由于学习的都是设计模式,所有系列文章都遵循如下的目录:

  • 模式档案:包含模式的定义、模式的特点、解决什么问题、优缺点、使用场景等
  • 模式结构:包含模式的角色定义及调用关系以及其模版代码
  • 模式示例:包含模式的实现方式代码举例,生活中的简单问题映射
  • 模式实践:如果工作中或开源项目用到了该模式,就将使用过程贴到这里,并且客观讨论使用的是否恰当
  • 模式对比:如果模式相似,有必要体现其相似点及不同点,区分使用,说明哪些场景下使用哪种模式比较好
  • 模式扩展:如果模式有与标准结构定义不同的变体形式,一并体现出其变体结构

接下来所有设计模式的介绍都暂且遵循此基本行文逻辑吗,如果某一条目没有则无需体现,但条目顺序遵循此结构

模式档案

在现实生活中,存在很多部分-整体的关系,例如,大学中的部门与学院、总公司中的部门与分公司等等,在软件开发中也是这样,例如,文件系统中的文件与文件夹窗体程序中的简单控件与容器控件等。对这些简单对象与复合对象的处理,如果用组合模式来实现会很方便。

模式定义:组合模式将一组对象组织(Compose)成树形结构,以表示一种部分 - 整体的层次结构。组合让客户端调用方可以统一单个对象和组合对象的处理逻辑

优点:组合模式有如下两个优点:

  1. 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,简化了客户端代码
  2. 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足开闭原则

缺点:使用场景较窄,只有处理的数据对象为树形结构才能发挥作用,通常可以应用于组织架构设计。

使用场景数据必须能表示成树形结构才能使用组合模式,这也导致了这种模式在实际的项目开发中并不那么常用。但是,一旦数据满足树形结构,应用这种模式就能发挥很大的作用,能让代码变得非常简洁

组合模式跟面向对象设计中的组合关系(通过组合来组装两个类),完全是两码事。这里讲的组合模式,主要是用来处理树形结构数据。这里的数据,可以简单理解为一组对象集合

模式结构

组合模式包含以下主要角色。

  • 抽象组件(Component)角色:它的主要作用是为树叶组件和树枝组件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象组件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝组件完成。(总的抽象类或接口,定义一些通用的方法,比如新增、删除)
  • 树叶组件(Leaf)角色:是组合中的叶节点对象,它没有子节点,用于继承或实现抽象组件。
  • 树枝组件(Composite)角色 / 中间组件:是组合中的分支节点对象,它有子节点,用于继承和实现抽象组件。它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法。

组合模式分为透明式的组合模式安全式的组合模式。透明式的组合模式结构如下:

1 透明式组合模式

在该方式中,由于抽象构件声明了所有子类中的全部方法,所以客户端无须区别树叶对象和树枝对象,对客户端来说是透明的。但其缺点是:树叶构件本来没有 Add()、Remove() 及 GetChild() 方法,却要实现它们(空实现或抛异常),这样会带来一些安全性问题

可以看到树叶组件也实现了管理的方法,但是方法为空方法

2 安全式组合模式

在该方式中,将管理子构件的方法移到树枝构件中,抽象构件和树叶构件没有对子对象的管理方法,这样就避免了上一种方式的安全性问题,但由于叶子和分支有不同的接口,客户端在调用时要知道树叶对象和树枝对象的存在,所以失去了透明性

透明式和安全式的组合模式,唯一的区别就是抽象组件中是否添加树叶节点的管理方法:

  • 透明式添加,那么在基于接口进行使用时,使用者并不需要关心实际对象是树枝还是树叶
  • 组合式不添加,那么在基于接口进行使用时,要想使用树叶管理方法,还得声明对象为树枝对象,因为树叶对象并不具备管理方法。

这是二者的一个本质区别。

模式实现

分别实现透明式组合模式和安全式组合模式

透明式组合模式

各个角色代码如下:

1 抽象组件

//抽象构件
interface Component {
    public void add(Component c);
    public void remove(Component c);
    public Component getChild(int i);
    public void operation();
}

2 树叶组件

//树叶构件
class Leaf implements Component {
    private String name;
    public Leaf(String name) {
        this.name = name;
    }
    public void add(Component c) {
    }
    public void remove(Component c) {
    }
    public Component getChild(int i) {
        return null;
    }
    public void operation() {
        System.out.println("树叶" + name + ":被访问!");
    }
}

3 树枝组件

//树枝构件
class Composite implements Component {
    private ArrayList<Component> children = new ArrayList<Component>();
    public void add(Component c) {
        children.add(c);
    }
    public void remove(Component c) {
        children.remove(c);
    }
    public Component getChild(int i) {
        return children.get(i);
    }
    public void operation() {
        for (Object obj : children) {
            ((Component) obj).operation();
        }
    }
}

客户端调用如下:

public class CompositePattern {
    public static void main(String[] args) {
        Component twig0 = new Composite();
        Component twig1 = new Composite();
        Component leaf1 = new Leaf("1");
        Component leaf2 = new Leaf("2");
        Component leaf3 = new Leaf("3");
        twig0.add(leaf1);
        twig0.add(twig1);
        twig1.add(leaf2);
        twig1.add(leaf3);
        twig0.operation();
        //空方法,不执行
        leaf1.add(leaf2);
    }
}

打印结果如下:

树叶1:被访问!
树叶2:被访问!
树叶3:被访问!

安全式组合模式

各个角色代码如下:

1 抽象组件

修改 Component 代码,只保留层次的公共行为

//抽象构件
interface Component {
    public void operation();
}

2 树叶组件

//树叶构件
class Leaf implements Component {
    private String name;
    public Leaf(String name) {
        this.name = name;
    }
    public void operation() {
        System.out.println("树叶" + name + ":被访问!");
    }
}

3 树枝组件

//树枝构件
class Composite implements Component {
    private ArrayList<Component> children = new ArrayList<Component>();
    public void add(Component c) {
        children.add(c);
    }
    public void remove(Component c) {
        children.remove(c);
    }
    public Component getChild(int i) {
        return children.get(i);
    }
    public void operation() {
        for (Object obj : children) {
            ((Component) obj).operation();
        }
    }
}

修改客户端代码,将树枝构件类型更改为 Composite 类型,以便获取管理子类操作的方法

public class CompositePattern {
    public static void main(String[] args) {
        Composite twig0 = new Composite();
        Composite twig1 = new Composite();
        Component leaf1 = new Leaf("1");
        Component leaf2 = new Leaf("2");
        Component leaf3 = new Leaf("3");
        twig0.add(leaf1);
        twig0.add(twig1);
        twig1.add(leaf2);
        twig1.add(leaf3);
        twig0.operation();
    }
}

模式实践

我们有如下两个场景的实践:设计一个文件系统设计一个组织成本核算系统,以下实例组合模式为安全式组合模式。

设计一个文件系统

假设我们有这样一个需求:设计一个类来表示文件系统中的目录,能方便地实现下面这些功能:动态地添加、删除某个目录下的子目录或文件;统计指定目录下的文件个数;统计指定目录下的文件总大小。代码如下:

public class FileSystemNode {
  private String path;
  private boolean isFile;
  private List<FileSystemNode> subNodes = new ArrayList<>();
  public FileSystemNode(String path, boolean isFile) {
    this.path = path;
    this.isFile = isFile;
  }
  public int countNumOfFiles() {
    if (isFile) {
      return 1;
    }
    int numOfFiles = 0;
    for (FileSystemNode fileOrDir : subNodes) {
      numOfFiles += fileOrDir.countNumOfFiles();
    }
    return numOfFiles;
  }
  public long countSizeOfFiles() {
    if (isFile) {
      File file = new File(path);
      if (!file.exists()) return 0;
      return file.length();
    }
    long sizeofFiles = 0;
    for (FileSystemNode fileOrDir : subNodes) {
      sizeofFiles += fileOrDir.countSizeOfFiles();
    }
    return sizeofFiles;
  }
  public String getPath() {
    return path;
  }
  public void addSubNode(FileSystemNode fileOrDir) {
    subNodes.add(fileOrDir);
  }
  public void removeSubNode(FileSystemNode fileOrDir) {
    int size = subNodes.size();
    int i = 0;
    for (; i < size; ++i) {
      if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) {
        break;
      }
    }
    if (i < size) {
      subNodes.remove(i);
    }
  }
}

单纯从功能实现角度来说,上面的代码没有问题,已经实现了我们想要的功能。但是,如果我们开发的是一个大型系统,从扩展性(文件或目录可能会对应不同的操作)、业务建模(文件和目录从业务上是两个概念)、代码的可读性(文件和目录区分对待更加符合人们对业务的认知)的角度来说,我们最好对文件和目录进行区分设计,定义为 File 和 Directory 两个类

public abstract class FileSystemNode {
  protected String path;
  public FileSystemNode(String path) {
    this.path = path;
  }
  public abstract int countNumOfFiles();
  public abstract long countSizeOfFiles();
  public String getPath() {
    return path;
  }
}
public class File extends FileSystemNode {
  public File(String path) {
    super(path);
  }
  @Override
  public int countNumOfFiles() {
    return 1;
  }
  @Override
  public long countSizeOfFiles() {
    java.io.File file = new java.io.File(path);
    if (!file.exists()) return 0;
    return file.length();
  }
}
public class Directory extends FileSystemNode {
  private List<FileSystemNode> subNodes = new ArrayList<>();
  public Directory(String path) {
    super(path);
  }
  @Override
  public int countNumOfFiles() {
    int numOfFiles = 0;
    for (FileSystemNode fileOrDir : subNodes) {
      numOfFiles += fileOrDir.countNumOfFiles();
    }
    return numOfFiles;
  }
  @Override
  public long countSizeOfFiles() {
    long sizeofFiles = 0;
    for (FileSystemNode fileOrDir : subNodes) {
      sizeofFiles += fileOrDir.countSizeOfFiles();
    }
    return sizeofFiles;
  }
  public void addSubNode(FileSystemNode fileOrDir) {
    subNodes.add(fileOrDir);
  }
  public void removeSubNode(FileSystemNode fileOrDir) {
    int size = subNodes.size();
    int i = 0;
    for (; i < size; ++i) {
      if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) {
        break;
      }
    }
    if (i < size) {
      subNodes.remove(i);
    }
  }
}

设计一个组织成本核算系统

假设我们在开发一个 OA 系统(办公自动化系统)。公司的组织结构包含部门和员工两种数据类型。其中,部门又可以包含子部门和员工

我们希望在内存中构建整个公司的人员架构图(部门、子部门、员工的隶属关系),并且提供接口计算出部门的薪资成本(隶属于这个部门的所有员工的薪资和)。部门包含子部门和员工,这是一种嵌套结构,可以表示成树这种数据结构。计算每个部门的薪资开支这样一个需求,也可以通过在树上的遍历算法来实现。所以,从这个角度来看,这个应用场景可以使用组合模式来设计和实现

抽象组件:人力资源管理

public abstract class HumanResource {
  protected long id;
  protected double salary;
  public HumanResource(long id) {
    this.id = id;
  }
  public long getId() {
    return id;
  }
  public abstract double calculateSalary();
}

树叶节点:人员成本信息

public class Employee extends HumanResource {
  public Employee(long id, double salary) {
    super(id);
    this.salary = salary;
  }
  @Override
  public double calculateSalary() {
    return salary;
  }
}

树枝节点:部门成本信息

public class Department extends HumanResource {
  private List<HumanResource> subNodes = new ArrayList<>();
  public Department(long id) {
    super(id);
  }
  @Override
  public double calculateSalary() {
    double totalSalary = 0;
    for (HumanResource hr : subNodes) {
      //递归计算薪资
      totalSalary += hr.calculateSalary();
    }
    this.salary = totalSalary;
    return totalSalary;
  }
  public void addSubNode(HumanResource hr) {
    subNodes.add(hr);
  }
}

客户端调用逻辑

public class Demo {
  private static final long ORGANIZATION_ROOT_ID = 1001;
  private DepartmentRepo departmentRepo; // 依赖注入
  private EmployeeRepo employeeRepo; // 依赖注入
  public void buildOrganization() {
    Department rootDepartment = new Department(ORGANIZATION_ROOT_ID);
    buildOrganization(rootDepartment);
  }
  // 构建组织架构的代码
  private void buildOrganization(Department department) {
    List<Long> subDepartmentIds = departmentRepo.getSubDepartmentIds(department.getId());
    for (Long subDepartmentId : subDepartmentIds) {
      Department subDepartment = new Department(subDepartmentId);
      department.addSubNode(subDepartment);
      //递归构建组织
      buildOrganization(subDepartment);
    }
    List<Long> employeeIds = employeeRepo.getDepartmentEmployeeIds(department.getId());
    for (Long employeeId : employeeIds) {
      double salary = employeeRepo.getEmployeeSalary(employeeId);
      department.addSubNode(new Employee(employeeId, salary));
    }
  }
}

总结一下

组合模式的场景使用较为特殊,实际上就是树这种数据结构组织的对象,通过抽象复杂数据结构中的通用方法使这个复杂对象调用起来和简单对象一样容易,例如核算一个部门的成本数据,对于部门+人这类组织数据就可以理所当然的认为是一个树形结构,无论是部门这种树枝还是人这种树叶都提供计算成本的统一方法,只要构建完毕这样一个组织,那么无论是计算哪一级的成本,都统一调用计算方法即可,递归逻辑已经内置在组织构建过程中了。

相关文章
|
2月前
|
设计模式 存储 缓存
【设计模式】【结构型模式】享元模式(Flyweight)
一、入门 什么是享元模式? 享元模式(Flyweight Pattern)是一种结构型设计模式,旨在通过共享对象来减少内存使用,特别适用于存在大量相似对象的情况。 它的核心思想是将对象的内在状态(不变
109 16
|
2月前
|
设计模式 缓存 安全
【高薪程序员必看】万字长文拆解Java并发编程!(8):设计模式-享元模式设计指南
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的经典对象复用设计模式-享元模式,废话不多说让我们直接开始。
65 0
|
2月前
|
设计模式 Java 数据库连接
【设计模式】【结构型模式】代理模式(Proxy)
一、入门 什么是代理模式? 代理模式(Proxy Pattern)是一种结构型设计模式,允许你提供一个代理对象来控制对另一个对象的访问。 代理对象在客户端和目标对象之间起到中介作用,可以在不改变目标对
75 10
|
2月前
|
设计模式 Java 定位技术
【设计模式】【结构型模式】组合模式(Composite)
一、入门 什么是组合模式 组合模式(Composite Pattern)是一种结构型设计模式,它允许你将对象组合成树形结构来表示“部分-整体”的层次关系。组合模式使得客户端可以统一处理单个对象和组合对
93 10
|
2月前
|
设计模式 Java 数据库连接
【设计模式】【结构型模式】外观模式(Facde)
一、入门 什么是外观模式? 一种结构型设计模式,通过为子系统中的一组接口提供一个统一的高层接口(称为外观),来简化客户端与复杂子系统的交互过程。其本质是建立抽象层来隔离复杂度。 为什么要有外观模式?
89 9
|
2月前
|
关系型数据库 Java MySQL
【设计模式】【结构型模式】桥接模式(Bridge)
一、入门 什么是桥接模式? 桥接模式(Bridge Pattern)是一种结构型设计模式,核心思想是将抽象与实现分离,让它们可以独立变化。简单来说,它像一座“桥”连接了两个维度的变化,避免用继承导致代
161 10
|
2月前
|
设计模式 前端开发 Java
【设计模式】【结构型模式】适配器模式(Adpter)
一、入门 什么是适配器模式? 适配器模式是Java中常用的结构型设计模式,它的核心作用就像现实中的电源转换器一样---让原本不兼容的两个接口能够协同工作。 为什么要用适配器模式? 假设我们需要在电商系
92 10
|
2月前
|
设计模式 缓存 安全
【设计模式】【结构型模式】装饰者模式(Decorator)
一、入门 什么是装饰者模式? 装饰者模式(Decorator Pattern)是 Java 中常用的结构型设计模式,它能在不修改原有对象结构的前提下,动态地为对象添加额外的职责。 为什么要装饰者模式?
59 8
|
4月前
|
设计模式 Java 数据安全/隐私保护
Java 设计模式:装饰者模式(Decorator Pattern)
装饰者模式属于结构型设计模式,允许通过动态包装对象的方式为对象添加新功能,提供比继承更灵活的扩展方式。该模式通过组合替代继承,遵循开闭原则(对扩展开放,对修改关闭)。
|
7月前
|
存储 Java 数据挖掘
Java 8 新特性之 Stream API:函数式编程风格的数据处理范式
Java 8 引入的 Stream API 提供了一种新的数据处理方式,支持函数式编程风格,能够高效、简洁地处理集合数据,实现过滤、映射、聚合等操作。
208 6