【组合模式】

127 阅读5分钟

定义

  • 组合模式(Composite Pattern)是一种结构型设计模式
  • 它允许你将对象组合成树形结构以表示部分 - 整体层次结构
  • 组合模式使得用户对单个对象和组合对象的使用具有一致性。

以下是组合模式的主要特点和定义:

一、主要角色

  1. Component(抽象构件)

    • 定义了组合中对象的接口,用于规范叶子节点和容器节点的公共操作,使得客户端可以以统一的方式处理叶子节点和容器节点。
    • 通常包含一些通用的方法,如添加子节点、删除子节点、获取子节点等操作的声明。
  2. Leaf(叶子构件)

    • 代表组合中的叶子节点,是最基本的、不能再包含其他子节点的对象。
    • 实现了 Component 接口,提供具体的业务逻辑。
  3. Composite(容器构件)

    • 代表组合中的容器节点,包含子节点,可以对这些子节点进行管理。
    • 实现了 Component 接口,除了实现自身的业务逻辑外,还负责管理子节点,如添加、删除、遍历子节点等操作。

二、使用场景

  1. 当你需要表示对象的部分 - 整体层次结构时,组合模式可以很方便地构建这样的树形结构。例如,文件系统中的目录和文件、图形用户界面中的容器和组件等。
  2. 当客户端希望以统一的方式处理单个对象和组合对象时,组合模式可以提供一致的接口,使得客户端无需区分处理的是叶子节点还是容器节点。

业务

类图

Folder持有-components : List属性,可以添加任意Component组件,组合成目标对象

classDiagram
    class Component {
        +display() : void
    }
    class File {
        -name : String
        +File(String)
        +display() : void
    }
    class Folder {
        -name : String
        -components : List<Component>
        +Folder(String)
        +addComponent(Component) : void
        +removeComponent(Component) : void
        +display() : void
    }
    File --|> Component
    Folder --|> Component

代码

用 Java 实现的组合模式的示例,假设我们有一个文件系统的模拟场景,包括文件夹和文件。

首先定义抽象构件 Component

public abstract class Component {
    public abstract void display();
}

然后实现叶子构件 Leaf(文件):

public class File extends Component {
    private String name;

    public File(String name) {
        this.name = name;
    }

    @Override
    public void display() {
        System.out.println("文件:" + name);
    }
}

接着实现容器构件 Composite(文件夹):

import java.util.ArrayList;
import java.util.List;

public class Folder extends Component {
    private String name;
    private List<Component> components;

    public Folder(String name) {
        this.name = name;
        components = new ArrayList<>();
    }

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

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

    @Override
    public void display() {
        System.out.println("文件夹:" + name);
        for (Component component : components) {
            component.display();
        }
    }
}

最后使用这些类:

public class Main {
    public static void main(String[] args) {
        Folder rootFolder = new Folder("根文件夹");
        File file1 = new File("文件 1.txt");
        File file2 = new File("文件 2.txt");
        Folder subFolder = new Folder("子文件夹");
        File subFile = new File("子文件.txt");

        rootFolder.addComponent(file1);
        rootFolder.addComponent(file2);
        rootFolder.addComponent(subFolder);

        subFolder.addComponent(subFile);

        rootFolder.display();
    }
}

在这个示例中,Component 是抽象构件,定义了所有组件的通用接口 display()File 类是叶子构件,代表文件,实现了 display() 方法来显示文件的名称。Folder 类是容器构件,代表文件夹,它可以包含其他组件(文件或文件夹),实现了 display() 方法来显示文件夹的名称,并遍历显示其包含的所有组件。这样,客户端可以以统一的方式处理文件和文件夹,无需区分它们的具体类型。

在框架中的使用

spring

一、配置类与 Bean 的管理

Spring 的配置类可以看作是一个容器构件,其中的 @Bean 方法创建的各个 Bean 对象是叶子构件。配置类负责将多个 Bean 组合在一起,形成一个完整的应用程序配置。

例如:

@Configuration
public class AppConfig {

    @Bean
    public ServiceA serviceA() {
        return new ServiceAImpl();
    }

    @Bean
    public ServiceB serviceB() {
        return new ServiceBImpl();
    }

    @Bean
    public CompositeService compositeService() {
        return new CompositeServiceImpl(serviceA(), serviceB());
    }
}

这里,CompositeService 依赖于 ServiceA 和 ServiceB,通过配置类将它们组合在一起,形成一个更复杂的服务。

二、Spring Security 中的过滤器链

Spring Security 使用过滤器链来处理请求的安全认证和授权。过滤器链是一个由多个过滤器组成的结构,其中每个过滤器可以看作是叶子构件,而过滤器链本身可以看作是容器构件。

例如:

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

public class SecurityConfig {

    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.addFilterBefore(new CustomFilter1(), UsernamePasswordAuthenticationFilter.class);
        http.addFilterBefore(new CustomFilter2(), CustomFilter1.class);
        return http.build();
    }
}

这里,通过 addFilterBefore 方法将多个过滤器组合成一个过滤器链,对请求进行依次处理,体现了组合模式的思想。

mybatis

在 MyBatis 中也能体现出组合模式的使用,主要体现在动态 SQL 的构建上。 MyBatis 的动态 SQL 允许根据不同的条件拼接 SQL 语句片段,就像组合不同的部分来构建一个完整的 SQL 语句。例如,使用 <if><choose><when><otherwise> 等标签来构建动态条件查询。

以下是一个示例:

<select id="findUsersByConditions" resultType="User">
    SELECT * FROM users
    WHERE 1 = 1
    <if test="username!= null">
        AND username = #{username}
    </if>
    <if test="age!= null">
        AND age = #{age}
    </if>
</select>

在这个例子中可以把整个动态 SQL 片段看作是一个组合结构。 <select> 标签和其中的固定部分(如 SELECT * FROM users WHERE 1 = 1)可以看作是容器构件, 而 <if> 标签内部的条件片段(如 AND username = #{username})可以看作是叶子构件。根据不同的输入条件,这些片段会被组合在一起,形成一个完整的 SQL 查询语句。

这样的设计使得 MyBatis 能够灵活地构建各种复杂的 SQL 语句,而无需为每一种可能的条件组合编写单独的 SQL 语句,提高了代码的可维护性和可扩展性。

总结

优点

  1. 简化客户端代码:客户端可以以统一的方式处理叶子节点和容器节点,无需区分它们的具体类型,提高了代码的可维护性和可扩展性。
  2. 易于扩展:可以方便地增加新的叶子节点或容器节点,而不会影响现有的代码结构。
  3. 更好地表示层次结构:清晰地表示了对象的部分 - 整体层次关系,使得代码结构更加直观。

缺点

  1. 实现较为复杂:组合模式需要定义多个类,并且需要处理好叶子节点和容器节点的关系,实现起来相对复杂。
  2. 可能会导致过度设计:如果层次结构比较简单,使用组合模式可能会使设计过于复杂,反而增加了代码的维护难度。