模式组合应用-组合模式

写在前面

Hello,我是易元 ,这篇文章是我学习设计模式时的笔记和心得体会。如果其中有错误,欢迎大家留言指正!

本文为设计模式间的组合使用,涉及代码较多,个人觉得熟能生巧,希望自己能从中学习到新的思维、新的灵感,希望大家也能有所收获。


组合模式

定义

组合模式是一种结构型设计模式,它旨在将对象组合成树形结构以表示 "部分-整体" 的层次结构,使得客户端使用单个对象或组合对象时具有一致性。

在软件开发中,经常会遇到处理树形结构数据的情况,最常见的莫过于组织架构中的部门和员工,经常在系统管理模块中存在。在传统设计中,处理树形结构时,客户端需要区分叶子节点和组合节点,导致代码中充满条件判断,组合模式通过引入一个共同的接口或抽象类来解决这个问题。该接口或抽象类定义了所有节点都必须实现的操作,这样客户端代码就可以统一地对待单个对象和组合对象,无需关心它们是 "部分" 还是 "整体" 这种方式极大的简化了客户端代码,提高了系统的可扩展性和灵活性。

角色

组合模式的核心思想是 "统一对待"。将 "部分" 和 "整体" 抽象为具有相同接口的对象,从而使得客户端代码可以透明的操作。

  • 组件 : 组合模式中的核心抽象, 定义了叶子节点和组合节点的共同接口, 这个接口通常包含业务方法以及管理子组件的方法(如 add()remove()getChild() 等 )。
  • 叶子节点: 代表树形结构中最小单元没有子节点, 叶子节点实现了组件接口中定义的业务方法, 但通常不实现或简单实现管理子组件的方法。
  • 组合节点: 代表树形结构中的复合元素, 可以包含子节点(可以是叶子节点, 也可以是其他组合节点)组合节点实现了组件接口中的所有方法, 包括业务方法和管理子组件的方法, 它通常会维护一个子组件的集合, 并将业务操作委托给其子组件。

使用时主要思考点

1.识别 "部分-整体" 层次结构

确保应用的核心模型能够以树状结构表示, 并且客户端需要统一处理单个对象和组合对象的场景, 如果系统中不存在这种层次结构, 或者客户端不需要统一处理, 那么组合模式可能并不适合该应用。

2. 定义共同的接口

组件接口的设计至关重要, 它应该包含所有叶子节点和组合节点都需要的业务操作, 以及组合节点管理子组件的操作。

3. 管理子组件

组合节点需要有效管理其子组件, 通常会涉及到添加、删除、获取子组件的方法, 在实现这些方法时, 需考虑线程安全、性能状况及如何处理循环引用等问题。

4. 递归操作

组合模式的业务操作通常是递归的, 组合节点会将操作委托给其子组件, 子组件中可能将操作委托给它的子组件, 直到操作到达叶子节点, 在实现递归操作时, 需要注意避免无限循环和栈溢出问题。


组合模式复习

场景

在一个文件系统中, 它由文件(File) 和 文件夹(Folder) 组成。文件夹中可以包含文件, 也可以包含其它文件夹, 形成一个典型的树形结构。我们需要实现如下功能, 能够计算任意给定文件夹的总大小, 或列出其所有内容。

实现思路

在代码设计中, 将文件抽象为叶子节点, 将文件夹抽象为组合节点, 两种类型实现组件类, 对组件类中定义的接口进行实现。

  • FileSystemComponent: 组件类, 定义文件和文件夹的共同接口。
  • File: 叶子节点类, 代表文件系统中的文件, 为文件系统中的最小单位。
  • Folder: 组合节点类, 代表文件系统中的文件夹, 可以包含其他组件, 并负责维护子组件列表, 将实际操作委托给子组件。

代码内容

组件接口
csharp 复制代码
public interface FileSystemComponent {

    String getName();

    long getSize();

    void display(String indent);

    default void add(FileSystemComponent component) {
        throw new UnsupportedOperationException("叶子节点无法添加子组件!");
    }

    default void remove(FileSystemComponent component) {
        throw new UnsupportedOperationException("叶子节点无法操作子组件!");
    }

    default FileSystemComponent getChild(int i) {
        throw new UnsupportedOperationException("叶子节点不包含子组件!");
    }

}
  • 组合模式的核心抽象, 定义了文件系统中的所有组件都必须遵循的契约。
  • 包含 getName() 获取组件名称, getSize() 获取组件大小, display(String indent) 以缩进格式显示组件的名称和大小。
  • add()remove()getChild() 管理子组件的系列方法。
叶子节点类
arduino 复制代码
public class File implements FileSystemComponent {

    private String name;

    private long size;

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

    @Override
    public String getName() {
        return name;
    }

    @Override
    public long getSize() {
        return size;
    }

    @Override
    public void display(String indent) {
        System.out.println(indent + "- File: " + name + " (" + size + "KB)");
    }

}
  • 代表文件系统中的单个文件, 是树形结构中不可再分的最小单元, 实现 FileSystemComponent 接口,提供文件的具体行为, 如返回自身大小、显示文件信息。
  • 未对子组件的操作方法进行实现, 叶子节点是最小单元, 不包含其他组件。
组合节点类
typescript 复制代码
public class Folder implements FileSystemComponent {

    private String name;

    private List<FileSystemComponent> children = new ArrayList<>();

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

    @Override
    public String getName() {
        return name;
    }

    @Override
    public long getSize() {
        long totalSize = 0;

        for (FileSystemComponent component : children) {
            totalSize += component.getSize();
        }

        return totalSize;
    }

    @Override
    public void display(String indent) {
        System.out.println("+ Folder: " + name + " (Total Size: " + getSize() + "KB)");
        for (FileSystemComponent component : children) {
            component.display(indent + "  ");
        }
    }

    @Override
    public void add(FileSystemComponent component) {
        children.add(component);
    }

    @Override
    public void remove(FileSystemComponent component) {
        children.remove(component);
    }

    @Override
    public FileSystemComponent getChild(int i) {
        return children.get(i);
    }
}
  • 代表文件系统中的文件夹, 可以包含其他 FileSystemComponent 对象。
  • 负责管理其他子组件, 实现了 FileSystemComponent 中定义的子组件管理方法。
测试类
ini 复制代码
public class FileSystemTest {

    @Test
    public void file_test() {

        File file1 = new File("document.txt", 100);
        File file2 = new File("image.jpg", 500);
        File file3 = new File("report.pdf", 200);
        File file4 = new File("config.ini", 50);

        Folder root = new Folder("Root");
        Folder documents = new Folder("Document");
        Folder pictures = new Folder("Pictures");

        documents.add(file1);
        documents.add(file3);

        pictures.add(file2);

        root.add(documents);
        root.add(pictures);
        root.add(file4);

        System.out.println("\n--- 显示当前文件系统结构和大小 ---");
        root.display("");

        System.out.println();
        System.out.println("Root文件夹总大小为: " + root.getSize() + "KB");

        System.out.println("Documents文件夹总大小为: " + documents.getSize() + "KB");
    }

}
  • 运行结果
arduino 复制代码
--- 显示当前文件系统结构和大小 ---
+ Folder: Root (Total Size: 850KB)
+ Folder: Document (Total Size: 300KB)
    - File: document.txt (100KB)
    - File: report.pdf (200KB)
+ Folder: Pictures (Total Size: 500KB)
    - File: image.jpg (500KB)
  - File: config.ini (50KB)

Root文件夹总大小为: 850KB
Documents文件夹总大小为: 300KB

Process finished with exit code 0

组合模式+迭代器模式

迭代器模式(行为型设计模式)

提供了一种顺序访问聚合对象中各个元素的方法, 而无需暴露该对象的内部表示, 它将遍历的职责从聚合对象中分离出来, 使得聚合对象和遍历逻辑可以独立变化。

案例

在文件系统操作的场景中, 使用组合模式构建了文件和文件夹的树形结构。现在, 我们希望能够以统一的方式遍历这个文件系统中的所有文件和文件夹而不仅仅是递归的显示它们。 例如,我们可能需要查找特定类型的文件, 或者对所有文件执行某种操作, 如果直接在 Folder 类中实现各种遍历逻辑会导致 Folder 类变得臃肿且职责不单一。

模式职责

  • 组合模式: 负责构建和维护文件和文件夹的树形结构, 使得客户端可以透明地操作单个文件和文件夹。
  • 迭代器模式: 负责提供一种统一的、不暴露内部结构的方式来遍历组合模式构建的树形结构中的所有元素。

具体代码结构

组合模式关联类
  • FileSystemComponent: 组件接口, 定义了 getName() getSize() display() 等操作。
  • File: 叶子节点, 实现了 FileSystemComponent 接口。
  • Folder: 组合节点, 实现了 FileSystemComponent 接口, 并维护一个子组件列表。
迭代器模式关联类
  • Iterator: Java内置接口, 定义了 hasNext()next() 方法。
  • Iterable: Java内置接口, 定义了 iterator() 方法, 使得对象可以通过 for-each 循环遍历。
  • FileSystemIterator: 具体的迭代器实现, 用于遍历 Folder 中的子组件, 为了实现深度遍历(例如, 广度优先或深度优先), 这个迭代器需要维护一个内部状态来跟踪遍历的进度。

代码内容

组件接口(未变更)
csharp 复制代码
public interface FileSystemComponent {

    String getName();

    long getSize();

    void display(String indent);

    default void add(FileSystemComponent component) {
        throw new UnsupportedOperationException("叶子节点不包含子组件列表! ");
    }

    default void remove(FileSystemComponent component) {
        throw new UnsupportedOperationException("叶子节点不包含子组件列表! ");
    }

    default FileSystemComponent getChild(int i) {
        throw new UnsupportedOperationException("叶子节点不包含子组件列表! ");
    }


}
  • 组合模式的核心抽象,定义了文件系统中的所有组件都必须遵循的契约。
  • 包含 getName() 获取组件名称, getSize() 获取组件大小, display(String indent) 以缩进格式显示组件的名称和大小。
  • add()remove()getChild() 管理子组件的系列方法。
叶子节点类(未变更)
arduino 复制代码
public class File implements FileSystemComponent {

    private String name;

    private long size;

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

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public long getSize() {
        return this.size;
    }

    @Override
    public void display(String indent) {
        System.out.println(indent + "- File: " + name + " (" + size + "KB)");
    }
}
  • 代表文件系统中的单个文件,是树形结构中不可再分的最小单元,实现 FileSystemComponent 接口, 提供文件的具体行为, 如返回自身大小、显示文件信息。
  • 未对子组件的操作方法进行实现, 叶子节点是最小单元, 不包含其他组件。
组合节点类
typescript 复制代码
public class Folder implements FileSystemComponent, Iterable<FileSystemComponent> {

    private String name;

    private List<FileSystemComponent> children = new ArrayList<>();

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

    /**
     * 返回迭代器对象
     *
     * @return
     */
    @Override
    public Iterator<FileSystemComponent> iterator() {
        return new DepthFirstIterator(children.iterator());
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public long getSize() {

        long totalSize = 0;

        for (FileSystemComponent component : children) {
            totalSize += component.getSize();
        }

        return totalSize;
    }

    @Override
    public void display(String indent) {

        System.out.println(indent + "+ Folder: " + name + " (Total Size: " + getSize() + "KB)");
        for (FileSystemComponent component : children) {
            component.display(indent + "  ");
        }

    }

    @Override
    public void add(FileSystemComponent component) {
        children.add(component);
    }

    @Override
    public void remove(FileSystemComponent component) {
        children.remove(component);
    }

    @Override
    public FileSystemComponent getChild(int i) {
        return children.get(i);
    }

    private static class DepthFirstIterator implements Iterator<FileSystemComponent> {

        // 使用栈来保存迭代器,实现深度优先遍历
        private Stack<Iterator<FileSystemComponent>> stack = new Stack<>();

        // 当前指向的文件系统组件
        private FileSystemComponent currentComponent;

        // 接收一个初始迭代器
        public DepthFirstIterator(Iterator<FileSystemComponent> iterator) {
            // 将初始迭代器压入栈
            stack.push(iterator);
            // 查找并设置第一个组件
            findNextComponent();
        }

        @Override
        public boolean hasNext() {
            // 如果 currentComponent 不为null,表示还有下一个元素
            return currentComponent != null;
        }

        @Override
        public FileSystemComponent next() {

            // 若没有下一个元素则抛出异常
            if (!hasNext()) {
                throw new NoSuchElementException("");
            }

            // 保存当前组件作为返回值
            FileSystemComponent nextComponent = currentComponent;
            // 查找下一个组件
            findNextComponent();
            // 返回之前保存的组件
            return nextComponent;
        }

        private void findNextComponent() {
            // 先将当前组件设置为null
            currentComponent = null;

            // 当栈不为空时进入循环
            while (!stack.empty()) {

                // 获取栈顶的迭代器
                Iterator<FileSystemComponent> iterator = stack.peek();

                // 存在下一个元素
                if (iterator.hasNext()) {

                    // 获取下一个组件
                    FileSystemComponent component = iterator.next();

                    // 如果组件是文件夹
                    if (component instanceof Folder) {

                        // 将文件夹的迭代器压入栈
                        stack.push(((Folder) component).iterator());
                    }

                    // 设置当前组件并返回
                    currentComponent = component;
                    return;
                }
                else {
                    // 当前迭代器没有元素,弹出栈
                    stack.pop();
                }

            }
        }
    }
}
  • 除了作为组合节点管理子组件外, 还实现了 Iterable<T> 接口, 使得 Folder 对象可以直接用于 for 循环。
  • iterator()Iterable<T> 接口方法, 返回一个 Iterator 实例, 负责定义遍历 Folder 中保存的子组件逻辑, 代码中返回了一个DepthFirstIterator 实例, 实现了深度优先遍历逻辑。
  • DepthFirstIterator 私有内部类, 实现了 Iterator 接口, 负责对文件系统树进行深度优先遍历。它维护了一个栈来管理待访问的迭代器, 从而实现递归遍历的效果。
测试类
ini 复制代码
public class FileTest {

    @Test
    public void test_file() {
        File file1 = new File("document.txt", 100);
        File file2 = new File("image.jpg", 500);
        File file3 = new File("report.pdf", 200);
        File file4 = new File("config.ini", 50);

        Folder root = new Folder("Root");
        Folder documents = new Folder("Documents");
        Folder pictures = new Folder("Pictures");
        Folder subDocuments = new Folder("SubDocuments");

        subDocuments.add(file4);
        documents.add(file1);
        documents.add(file3);
        documents.add(subDocuments);

        pictures.add(file2);

        root.add(documents);
        root.add(pictures);

        System.out.println("\n--- 显示文件系统结构 ---\n");
        root.display("");

        System.out.println("\n--- 遍历文件系统(深度优先) ---\n");
        for (FileSystemComponent component : root) {
            System.out.println("Found: " + component.getName() + " (Size: " + component.getSize() + "KB)");
        }

        System.out.println("\n--- 查找所有PDF文件 ---\n");
        for (FileSystemComponent component : root) {
            if (component instanceof File && component.getName().endsWith(".pdf")) {
                System.out.println("PDF File: " + component.getName() + "(Size: " + component.getSize() + "KB)");
            }
        }
    }

}
  • 运行结果
less 复制代码
--- 显示文件系统结构 ---

+ Folder: Root (Total Size: 850KB)
  + Folder: Documents (Total Size: 350KB)
    - File: document.txt (100KB)
    - File: report.pdf (200KB)
    + Folder: SubDocuments (Total Size: 50KB)
      - File: config.ini (50KB)
  + Folder: Pictures (Total Size: 500KB)
    - File: image.jpg (500KB)

--- 遍历文件系统(深度优先) ---

Found: Documents (Size: 350KB)
Found: document.txt (Size: 100KB)
Found: report.pdf (Size: 200KB)
Found: SubDocuments (Size: 50KB)
Found: config.ini (Size: 50KB)
Found: config.ini (Size: 50KB)
Found: Pictures (Size: 500KB)
Found: image.jpg (Size: 500KB)

--- 查找所有PDF文件 ---

PDF File: report.pdf(Size: 200KB)

Process finished with exit code 0

组合优势

  • 职责分离: 组合模式负责构建属性结构, 迭代器负责遍历该结构。使得 Folder 类不再需要包含复杂的遍历逻辑, 从而保证其职责的单一性。
  • 遍历策略的灵活性: 可以轻松的替换或添加新的迭代器实现, 而无需修改 Folder 类或客户端代码, 从而提高系统的灵活性和扩展性。

组合模式+访问者模式

访问者模式(行为型设计模式)

它允许在不改变现有对象结构的前提下, 向该对象结构中添加新的操作。当需要对一个对象结构中的元素执行多种不同的、不相关的操作, 并且这些操作经常变化时, 访问者模式非常有用。它将操作的逻辑从对象结构中分离出来, 使得操作可以独立于对象结构而变化。

案例

文件系统中, 除了计算大小和显示结构, 我们可能还需要对文件系统中的文件和文件夹执行各种不同的操作, 例如: 权限检查、文件压缩、病毒扫描、文件备份等, 如果将这些操作直接添加到 FilsSystemComponent 接口及其实现类(FileFolder)中, 会导致接口变得臃肿, 而且职责变的复杂起来。

模式职责

  • 组合模式: 负责构建和维护文件和文件夹的树形结构, 使得客户端可以透明的操作单个文件和文件夹。
  • 访问者模式: 负责定义对组合模式构建的树形结构中各个元素(文件和文件夹)执行的各种操作, 并将这些操作的逻辑从元素类中分离出来。

具体代码结构

组合模式关联类
  • FileSystemComponent: 组件接口, 定义了 getName() getSize() display() 等操作, 添加 accept(FileSystemVisitor visitor)方法, 用于接受访问者。
  • File: 叶子节点, 实现了 FileSystemComponent 接口, 并实现 accept 方法, 调用访问者的 visitFile 方法。
  • Folder: 组合节点, 实现了 FileSystemComponent 接口并维护子组件列表, 调用访问者的 visitFolder 方法, 并递归地让其子组件接受访问者。
访问者模式关联类
  • FileSystemVisitor: 定义了针对 FileFoldervisit 方法重载, 每个具体操作都将实现这个接口。
  • PermissionCheckVisitor: 具体的访问者, 实现 FileSystemVisitor 接口, 用于执行权限检查操作。
  • CompressionVisitor: 具体的访问者, 实现 FileSystemVisitor 接口, 用于执行压缩操作。

代码内容

组件接口(变更)
csharp 复制代码
public interface FileSystemComponent {

    String getName();

    long getSize();

    void display(String indent);

    void accept(FileSystemVisitor visitor);

    default void add(FileSystemComponent component){
        throw new UnsupportedOperationException("叶子节点不包含子组件! ");
    }

    default void remove(FileSystemComponent component){
        throw new UnsupportedOperationException("叶子节点不包含子组件! ");
    }

    default FileSystemComponent getChild(int i){
        throw new UnsupportedOperationException("叶子节点不包含子组件! ");
    }
}
  • 在原来的基础上新增了 void accept(FileSystemVisitor visitor) 方法。文件系统组件接收一个访问者对象, 当调用此方法时, 组件会回调访问者对象中对应其类型的方法。
叶子节点类
arduino 复制代码
public class File implements FileSystemComponent {

    private String name;

    private long size;

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

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public long getSize() {
        return this.size;
    }

    @Override
    public void display(String indent) {
        System.out.println(indent + "- File: " + name + " (" + size + "KB)");
    }

    @Override
    public void accept(FileSystemVisitor visitor) {
        visitor.visit(this);
    }
}
  • 既代表组合模式中的叶子节点类, 也代表了访问者中的元素角色, 对新增的 accept() 方法进行了实现, 在实现中 File 对象调用传入的 visitor 对象的 visitFile(this) 方法, 将自身作为参数传递, 确保访问者能够针对 File 类型执行特定的操作。
组合节点类
typescript 复制代码
public class Folder implements FileSystemComponent {

    private String name;

    private List<FileSystemComponent> children = new ArrayList<>();

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

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public long getSize() {
        long totalSize = 0;
        for (FileSystemComponent component : children) {
            totalSize += component.getSize();
        }
        return totalSize;
    }

    @Override
    public void display(String indent) {
        System.out.println(indent + "+ Folder: " + name + " (文件大小: " + getSize() + "KB)");
        for (FileSystemComponent component : children) {
            component.display(indent + "  ");
        }
    }

    @Override
    public void add(FileSystemComponent component) {
        children.add(component);
    }

    @Override
    public void remove(FileSystemComponent component) {
        children.remove(component);
    }

    @Override
    public FileSystemComponent getChild(int i) {
        return children.get(i);
    }

    @Override
    public void accept(FileSystemVisitor visitor) {
        visitor.visit(this);
        for (FileSystemComponent component : children) {
            component.accept(visitor); // 递归传递给子组件接受访问者
        }
    }
}
  • 既代表组合模式中的叶子节点类, 也代表了访问者中的元素角色, 对新增的 accept() 方法进行了实现, 在实现逻辑中, Folder 对象首先调用传入的 visitor 对象的 visitFolder(this) 方法, 然后遍历其所有子组件, 并递归的调用每个子组件的 accept(visitor) 方法。确保访问者能够遍历整个文件系统树, 并对每个节点执行相应操作。
访问者接口
arduino 复制代码
public interface FileSystemVisitor {

    void visit(File visitor);

    void visit(Folder visitor);
}
  • 访问者模式的核心接口, 定义了针对文件系统组件的访问操作。它包含两个重载的 visit 方法, 分别访问 FileFolder
具体访问者类
typescript 复制代码
public class CompressionVisitor implements FileSystemVisitor {

    @Override
    public void visitFile(File visitor) {
        System.out.println("压缩文件: " + visitor.getName() + "...");
    }

    @Override
    public void visitFolder(Folder visitor) {
        System.out.println("压缩文件夹: " + visitor.getName() + "....");
    }
}
public class PermissionCheckVisitor implements FileSystemVisitor {

    private String user;

    public PermissionCheckVisitor(String user) {
        this.user = user;
    }

    @Override
    public void visitFile(File visitor) {
        System.out.println("校验文件权限: " + visitor.getName() + " 访问用户: " + user + " - 读写权限");
    }

    @Override
    public void visitFolder(Folder visitor) {
        System.out.println("校验文件夹权限: " + visitor.getName() + " 访问用户: " + user + " - 完全访问权限");
    }

}
  • PermissionCheckVisitor类实现了 FileSystemVisitor 接口, 它封装了对文件和文件夹进行权限检查的逻辑。当 visitFile 被调用时, 它执行文件的权限检查, 当 visitFolder 被调用时, 执行文件夹的权限检查。
  • CompressionVisitor类实现了 FileSystemVisitor 接口, 它封装了对文件和文件夹进行压缩的逻辑。当 visitFile 被调用时, 它模拟文件压缩, 当 visitFolder 被调用时, 它模拟文件夹打包压缩。
测试类
ini 复制代码
public class FileSystemTest {

    @Test
    public void test_file() {
        File file1 = new File("document.txt", 100);
        File file2 = new File("image.jpg", 500);
        File file3 = new File("report.pdf", 200);

        Folder root = new Folder("Root");
        Folder documents = new Folder("Documents");
        Folder pictures = new Folder("Pictures");

        documents.add(file1);
        documents.add(file3);

        pictures.add(file2);

        root.add(documents);
        root.add(pictures);

        System.out.println("\n--- 显示文件系统结构 ---\n");
        root.display("");

        System.out.println("\n--- 执行权限检查 ---\n");
        FileSystemVisitor permissionVisitor = new PermissionCheckVisitor("admin");
        root.accept(permissionVisitor);

        System.out.println("\n--- 执行文件压缩 ---\n");
        FileSystemVisitor compressionVisitor = new CompressionVisitor();
        root.accept(compressionVisitor);
    }
}
  • 运行结果
makefile 复制代码
--- 显示文件系统结构 ---

+ Folder: Root (文件大小: 800KB)
  + Folder: Documents (文件大小: 300KB)
    - File: document.txt (100KB)
    - File: report.pdf (200KB)
  + Folder: Pictures (文件大小: 500KB)
    - File: image.jpg (500KB)

--- 执行权限检查 ---

校验文件夹权限: Root 访问用户: admin - 完全访问权限
校验文件夹权限: Documents 访问用户: admin - 完全访问权限
校验文件权限: document.txt 访问用户: admin - 读写权限
校验文件权限: report.pdf 访问用户: admin - 读写权限
校验文件夹权限: Pictures 访问用户: admin - 完全访问权限
校验文件权限: image.jpg 访问用户: admin - 读写权限

--- 执行文件压缩 ---

压缩文件夹: Root....
压缩文件夹: Documents....
压缩文件: document.txt...
压缩文件: report.pdf...
压缩文件夹: Pictures....
压缩文件: image.jpg...

Process finished with exit code 0

组合优势

  • 通过与访问者模式的组合, 使得在不修改现有文件系统组件类的情况下添加新的操作(例如: 病毒扫描、文件备份)极大的提高了系统的可维护性和可扩展性。
  • 文件系统组件只负责维护其结构和基本行为, 而具体的业务操作逻辑则被封装在独立的访问者类中, 使得每个类的职责更加单一和清晰。

组合模式+生成器模式

生成器模式(创建型设计模式)

允许分步创建复杂对象, 该模式将对象的构建与其表示分离, 使得同样的构建过程可以创建不同的表示。当一个对象的构建过程非常复杂, 包含多个步骤, 并且这些步骤的顺序可以随时变化, 或需要创建不同表示的对象时, 生成器模式就可以很好的应用。

生成器模式的核心思想是将复杂对象的构建过程封装在一个独立的生成器对象中。客户端通过调用生成器对象的一系列方法来逐步构建对象, 最后通过 build() 方法获取最终的对象。使得构建过程更加清晰, 并且可以将构建逻辑与业务逻辑分离。

案例

假设我们需要开发一个系统, 用于生成各种报告、文章或书籍。这些文档通常具有复杂的层次结构, 例如:

  • 文章包含章节
  • 章节包含小节和段落
  • 小节包含子小节和段落
  • 段落包含文本、图片等基本元素

模式职责

  • 组合模式: 负责构建文档的层次结构, 它将文档中的基本元素和复合元素统一处理, 无论是单个对象还是对象的组合客户端都可以统一的操作。
  • 生成器模式: 负责分步、灵活的构建复杂的文档对象, 它将文档的构建过程(添加标题、段落、章节等)与文档的最终表示(树形结构)分离。通过生成器可以链式的调用来创建文档, 避免了构造函数参数过多或直接操作复杂组合对象的繁琐。

具体代码结构

组合模式关联类
  • DocumnetComponent: 组件角色, 定义文档中所有组件的通用接口。
  • Heading/Paragraph: 叶子节点角色, 表示文档中的基本元素, 例如: 标题和段落。它们没有子组件。
  • DocumentComposite: 组合节点角色, 表示文档中的复合元素, 例如: 章节或整个文档。
生成器模式关联类
  • DocumentBuilder: 生成器角色, 定义构建文档各个部分的抽象接口。
  • ConcreteDocumentBuilder: 具体生成器角色, 负责具体的文档构建逻辑。
  • DocumentComponent/Heading/Paragraph/DocumentComposite: 产品角色, 代表最终构建完成的文档结构。

代码内容

组件接口
csharp 复制代码
public interface DocumentComponent {

    void render();
}
  • 定义了 render() 方法, 声明了组件的通用接口, 是统一操作文档元素的关键。
叶子节点类
arduino 复制代码
// 标题
public class Heading implements DocumentComponent {

    private String text;

    private int level;

    public Heading(String text, int level) {
        this.text = text;
        this.level = level;
    }

    @Override
    public void render() {
        System.out.println(String.format("<%s>%s</%s>", "h" + level, text, "h" + level));
    }
}
// 段落
public class Paragraph implements DocumentComponent {

    private String text;

    public Paragraph(String text) {
        this.text = text;
    }

    @Override
    public void render() {
        System.out.println("<p>" + text + "</p>");
    }
}
  • Heading: 封装了标题的文本内容(text)和标题级别(level, 例如h1,h2,h3), render()方法内部逻辑是将标题以HTML标签的形式输出到控制台,模拟文档渲染。
  • Paragraph: 封装了段落的文本内容(text), render()方法负责将段落以 HTML <p> 标签的形式输出到控制台。
组合节点类
typescript 复制代码
public class DocumentComposite implements DocumentComponent {

    private List<DocumentComponent> children = new ArrayList<>();

    public void add(DocumentComponent component) {
        children.add(component);
    }

    public void remove(DocumentComponent component) {
        children.remove(component);
    }

    public DocumentComponent get(int i) {
        return children.get(i);
    }

    @Override
    public void render() {
        for (DocumentComponent component : children) {
            component.render();
        }
    }

}
  • DocumentComposite 内部维护了一个 List<DocumnetComponent> 来存储其子组件
  • add()remove()get(int i) 方法用于管理子组件的操作。
  • render() 方法实现了递归渲染, 它遍历其内部的 children 列表, 并依次调用每个子组件的 render() 方法。
生成器接口
arduino 复制代码
public interface DocumentBuilder {

    DocumentBuilder addHeading(String text, int level);

    DocumentBuilder addParagraph(String text);

    DocumentBuilder addComposite(DocumentComponent composite);

    DocumentComponent build();
}
  • 接口定义了添加标题、段落和复合组件的方法, 这些方法都返回 DocumentBuilder 自身, 从而支持链式调用。
  • build() 方法是构建过程中的组后一步, 它返回最终构建完成的 DocumnetComponent 对象。
具体生成器类
typescript 复制代码
public class ConcreteDocumentBuilder implements DocumentBuilder {

    private DocumentComposite document = new DocumentComposite();

    @Override
    public DocumentBuilder addHeading(String text, int level) {
        document.add(new Heading(text, level));
        return this;
    }

    @Override
    public DocumentBuilder addParagraph(String text) {
        document.add(new Paragraph(text));
        return this;
    }

    @Override
    public DocumentBuilder addComposite(DocumentComponent composite) {
        document.add(composite);
        return this;
    }

    @Override
    public DocumentComponent build() {
        return document;
    }
}
  • ConcreteDocumentBuilder 内部持有一个 DocumentComposite 实例, 作为正在构建的产品。
  • addHeading()addParagraph()addComposite() 方法根据传入的参数创建相应的 DocumentComponent 对象, 并将其添加到内部的 document 组合对象中。
  • build() 方法返回或者内部构建好的 document 组合对象。
测试类
java 复制代码
public class DocumentBuilderTest {

    @Test
    public void test_document_build() {
        DocumentComposite chapter1 = new DocumentComposite();
        chapter1.add(new Heading("第一章 组合模式简介", 1));
        chapter1.add(new Paragraph("组合模式允许将对象组合成树形结构来表示 部分-整体 的层次结构。"));

        DocumentComposite section1_1 = new DocumentComposite();
        section1_1.add(new Heading("1.1 组合模式的优势", 2));
        section1_1.add(new Paragraph("它使得客户端统一的处理单个对象和组合对象。"));

        chapter1.add(section1_1);

        DocumentBuilder builder = new ConcreteDocumentBuilder();
        DocumentComponent documentPart = builder.addHeading("第二章 生成器模式详解", 1)
                .addParagraph("生成器模式允许你分步骤创建复杂对象。")
                .addParagraph("它将对象的构建和其表示分离, 使得同样的构建过程可以创建不同的表示。")
                .build();

        DocumentBuilder combinedBuilder = new ConcreteDocumentBuilder();
        DocumentComponent documentComponent = combinedBuilder
                .addComposite(chapter1)
                .addComposite(documentPart)
                .addHeading("第三章 组合模式与生成器模式的融合", 1)
                .addParagraph("通过结合两种模式, 我们可以灵活的构建负责的文档结构。")
                .addParagraph("这种组合使得文档的创建变得灵活。")
                .build();

        documentComponent.render();
    }

}
  • 运行结果
css 复制代码
<h1>第一章 组合模式简介</h1>
<p>组合模式允许将对象组合成树形结构来表示 部分-整体 的层次结构。</p>
<h2>1.1 组合模式的优势</h2>
<p>它使得客户端统一的处理单个对象和组合对象。</p>
<h1>第二章 生成器模式详解</h1>
<p>生成器模式允许你分步骤创建复杂对象。</p>
<p>它将对象的构建和其表示分离, 使得同样的构建过程可以创建不同的表示。</p>
<h1>第三章 组合模式与生成器模式的融合</h1>
<p>通过结合两种模式, 我们可以灵活的构建负责的文档结构。</p>
<p>这种组合使得文档的创建变得灵活。</p>

Process finished with exit code 0

组合优势

  • 组合模式专注于定义和管理文档层次结构, 生成器模式专注于复杂文档对象的构建过程, 使得构建复杂文档的逻辑被划分到各自的模式中, 降低了单个模块的复杂性, 提高了代码的可读性和可维护性。
  • 组合模式使得程序可以轻松的添加新的文档元素类型, 生成器模式使得通过创建不同的具体生成器来构建不同类型或不同表示的文档, 两者结合,我们可以灵活的定义文档内部结构, 还可以灵活的控制文档的创建过程。

组合模式+责任链模式

责任链模式(行为型设计模式)

允许将请求沿着处理者链进行发送, 收到请求后每个处理者均可对请求进行处理, 或将其传递给链上的下一个处理者

案例

以企业IT工单处理系统为场景, 模拟组合模式与责任链模式的结合使用。系统需要处理简单工单和复合工单, 并实现多级自动配单和处理流程。

  • 工单类型: 简单工单, 单一的任务。复合工单, 有多个任务组成。
  • 处理流程: 一线支持人员处理简单问题, 负责问题自动升级为二线专家, 系统性问题升级到IT经理。

模式职责

  • 组合模式: 统一管理简单工单和复合工单, 定义工单的 部分-整体 层次结构。
  • 责任链模式: 定义工单请求的传递机制, 并实现请求的动态处理。将请求的发送者和接收者解耦。

具体代码结构

组合模式关联类
  • TicketComponent: 组件角色, 定义工单树中所有对象的通用接口。
  • SimpleTicket: 叶子节点角色, 表示简单工单类型, 工单的最小单元。
  • CompositeTicket: 组合节点角色, 可以包含多个简单工单和多个复杂工单。
责任链模式关联类
  • TicketHandler: 抽象处理者角色, 定义处理请求的接口, 并维护一个指向下一个处理者的引用。
  • FirstLineSupport: 具体处理者角色, 处理最简单、最常见的工单, 若无法处理, 则将请求传递给二线技术支持。
  • SecondLineSupport: 具体处理者角色, 处理比一线支持更复杂的工单, 如果无法处理, 则传递给IT经理。
  • ITManager: 具体处理者角色, 表示责任链的最终处理者, 处理所有未能被前序处理者解决的工单.

代码内容

组件接口
csharp 复制代码
public abstract class TicketComponent {

    protected String ticketId;

    protected String description;

    protected TicketComponent parent;

    protected TicketStatus status = TicketStatus.OPEN;

    public TicketComponent(String ticketId, String description) {
        this.ticketId = ticketId;
        this.description = description;
    }

    public void setParent(TicketComponent parent) {
        this.parent = parent;
    }

    public TicketComponent(TicketComponent parent) {
        this.parent = parent;
    }

    public abstract void add(TicketComponent component);

    public abstract void remove(TicketComponent component);

    public abstract void process(TicketHandler handler);

    public abstract boolean isCompleted();

    public abstract void display(String indent);

    public void updateStatus(TicketStatus status) {
        this.status = status;
        System.out.println("工单 " + ticketId + " 状态更新为: " + status);
    }
}
  • 包含四个成员变量: ticketId 工单标识, description 描述信息, parent 指向父工单的引用 允许子工单在必要时访问其父级(如: 通知父工单所有子工单处理完成), status 工单当前状态。
  • 包含子工单的一系列控制方法, add remove
  • process, 组合模式与责任链模式的结合点, 定义了工单如何被处理的抽象行为, 具体实现会将工单提交给责任链的起始处理者。
  • updateStatus 更新工单状态, display 显示工单信息及层级, isCompleted 判断工单是否完成。
叶子节点类
typescript 复制代码
public class SimpleTicket extends TicketComponent {

    public SimpleTicket(String ticketId, String description) {
        super(ticketId, description);
    }

    @Override
    public void add(TicketComponent component) {
        throw new UnsupportedOperationException("简单工单不能添加子工单");
    }

    @Override
    public void remove(TicketComponent component) {
        throw new UnsupportedOperationException("简单工单没有子工单");
    }

    @Override
    public void process(TicketHandler handler) {

        System.out.println("\n开始处理简单工单: " + description + " (ID: " + ticketId + ")");

        boolean handled = handler.handleTicket(this);

        if (!handled && parent != null) {
            System.out.println("当前处理人无法解决, 向上继工单传递");
            parent.process(handler);
        }

    }

    @Override
    public boolean isCompleted() {
        return status == TicketStatus.COMPLETED;
    }

    @Override
    public void display(String indent) {
        System.out.println(indent + " [简单工单] " + ticketId + ": " + description + " (" + status + ")");
    }
}
  • 实现父级方法, 对 add remove 方法内抛出异常, 表示简单工单不包含子工单。
  • process 进入责任链的入口, 直接调用传入的 TicketHandler 中的 handleTicket 方法, 将自身提交给责任链进行处理。
  • display 打印工单信息及状态。
组合节点类
typescript 复制代码
public class CompositeTicket extends TicketComponent {

    private List<TicketComponent> children = new ArrayList<>();

    public CompositeTicket(String ticketId, String description) {
        super(ticketId, description);
    }

    @Override
    public void add(TicketComponent component) {
        children.add(component);
        component.setParent(this);
    }

    @Override
    public void remove(TicketComponent component) {
        children.remove(component);
        component.setParent(null);
    }

    @Override
    public void process(TicketHandler handler) {

        System.out.println("\n开始处理复合工单: " + description + " (ID: " + ticketId + ")");
        for (TicketComponent child : children) {

            if (!child.isCompleted()) {
                child.process(handler);
            }
        }

        if (checkChildrenCompletion()) {
            this.updateStatus(TicketStatus.COMPLETED);
            System.out.println("复合工单 " + ticketId + " 所有子工单已完成");
        }
        else {
            this.updateStatus(TicketStatus.IN_PROGRESS);
        }

    }

    private boolean checkChildrenCompletion() {
        for (TicketComponent child : children) {

            if (!child.isCompleted()) {
                return false;
            }

        }

        return true;
    }

    @Override
    public boolean isCompleted() {
        return status == TicketStatus.COMPLETED;
    }

    @Override
    public void display(String indent) {
        System.out.println(indent + " [复合工单] " + ticketId + ": " + description + " (" + status + ")");
        for (TicketComponent child : children) {
            child.display("   ");
        }
    }
}
  • 实现 add, 将子工单添加到内部列表中, 并设置子工单的 parent 引用, remove 将内部列表中的子工单移除, 清除子工单的 parent 引用。
  • process 遍历内部列表中的所有子工单, 递归的调用子工单的 process 方法, 从而将每个子工单提交给责任链进行处理, 所有子工单执行完成后, 更新复合工单自身的 status
  • checkChildrenCompletion 私有辅助方法, 用于检查其子工单是否已经完成, 当所有子工单均为完成状态返回 true
  • display 递归的显示复合工单及其子工单的所有信息, 并增加缩进展示层级关系。
抽象处理类
java 复制代码
public interface TicketHandler {

    void setNextHandler(TicketHandler nextHandler);

    boolean handleTicket(TicketComponent ticket);
}
  • setNextHandler 设置责任链的下一个处理者, 构建责任链的关键方法。
  • handleTicket 处理工单请求的核心方法, 当前处理者成功处理后返回 true, 否则返回 false 并将请求传递给下一个处理者。
具体处理者类
typescript 复制代码
public class FirstLineSupport implements TicketHandler {

    private String name;

    private TicketHandler nextHandler;

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

    @Override
    public void setNextHandler(TicketHandler nextHandler) {
        this.nextHandler = nextHandler;
    }

    @Override
    public boolean handleTicket(TicketComponent ticket) {

        if (canHandle(ticket)) {
            System.out.println("一线支持人员 " + name + " 正在处理工单: " + ticket.description);
            ticket.updateStatus(TicketStatus.COMPLETED);
            return true;
        }
        else if (nextHandler != null) {
            System.out.println("一线支持人员 " + name + " 无法处理, 转交二线支持");
            return nextHandler.handleTicket(ticket);
        }

        return false;
    }

    private boolean canHandle(TicketComponent ticket) {
        return ticket instanceof SimpleTicket && !ticket.description.contains("服务器") && !ticket.description.contains("网络");
    }

}
public class SecondLineSupport implements TicketHandler {

    private String name;

    private TicketHandler nextHandler;

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

    @Override
    public void setNextHandler(TicketHandler nextHandler) {
        this.nextHandler = nextHandler;
    }

    @Override
    public boolean handleTicket(TicketComponent ticket) {
        if (canHandle(ticket)) {
            System.out.println("二线技术支持 " + name + " 正在处理工单: " + ticket.description);
            ticket.updateStatus(TicketStatus.COMPLETED);
            return true;
        }
        else if (nextHandler != null) {
            System.out.println("二线技术支持 " + name + "无法处理, 转交IT经理");
            return nextHandler.handleTicket(ticket);
        }

        return false;
    }

    private boolean canHandle(TicketComponent ticket) {
        return !ticket.description.contains("预算") && !ticket.description.contains("采购");
    }
}
public class ITManager implements TicketHandler {

    private String name;

    private TicketHandler nextHandler;

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

    @Override
    public void setNextHandler(TicketHandler nextHandler) {
        this.nextHandler = nextHandler;
    }

    @Override
    public boolean handleTicket(TicketComponent ticket) {
        System.out.println("IT经理 " + name + " 正在处理工单: " + ticket.description);
        ticket.updateStatus(TicketStatus.COMPLETED);
        return true;
    }
}
  • 包含两个成员变量: name 处理者名称, nextHandler 指向责任链中下一个处理者的引用。
  • 实现父级方法, 设置下一个处理者。
  • handleTicket 调用 canHandle 判断自己是否能够处理当前工单, 若能处理则更新工单状态并返回 true, 若不能处理并且 nextHandler不为空, 则将工单传递给下一个处理者( nextHandler.handleTicket(ticket) ), 若不能处理且没有下一个处理者, 则表示工单不能被处理, 并返回 false
测试类
ini 复制代码
public class TicketTest {

    @Test
    public void test_ticket() {
        TicketHandler manager = new ITManager("张经理");
        TicketHandler secondLine = new SecondLineSupport("李二线");
        secondLine.setNextHandler(manager);
        TicketHandler firstLine = new FirstLineSupport("王一线");
        firstLine.setNextHandler(secondLine);

        SimpleTicket ticket1 = new SimpleTicket("T1001", "重置用户密码");
        SimpleTicket ticket2 = new SimpleTicket("T1002", "办公室打印机故障");
        SimpleTicket ticket3 = new SimpleTicket("T1003", "服务器宕机处理");
        SimpleTicket ticket4 = new SimpleTicket("T1004", "采购服务器预算审批");

        CompositeTicket onboardingTicket = new CompositeTicket("T2001", "新员工入职准备");
        onboardingTicket.add(new SimpleTicket("ST2001", "创建邮箱账户"));
        onboardingTicket.add(new SimpleTicket("ST2002", "配置门禁卡"));
        onboardingTicket.add(new SimpleTicket("ST2003", "分配工位及办公用品"));

        CompositeTicket moveTicket = new CompositeTicket("T2002", "办公室搬迁IT支持");
        moveTicket.add(new SimpleTicket("ST2004", "网络布线"));
        moveTicket.add(new SimpleTicket("ST2005", "电话系统迁移"));

        System.out.println("--- 工单结构 ---");
        ticket1.display("");
        ticket2.display("");
        ticket3.display("");
        ticket4.display("");
        onboardingTicket.display("");
        moveTicket.display("");

        System.out.println("\n--- 简单工单处理 ---");
        ticket1.process(firstLine);
        ticket2.process(firstLine);
        ticket3.process(firstLine);
        ticket4.process(firstLine);

        System.out.println("\n--- 复杂工单处理 ---");
        onboardingTicket.process(firstLine);
        moveTicket.process(firstLine);
    }

}
  • 运行结果
less 复制代码
--- 工单结构 ---
 [简单工单] T1001: 重置用户密码 (OPEN)
 [简单工单] T1002: 办公室打印机故障 (OPEN)
 [简单工单] T1003: 服务器宕机处理 (OPEN)
 [简单工单] T1004: 采购服务器预算审批 (OPEN)
 [复合工单] T2001: 新员工入职准备 (OPEN)
    [简单工单] ST2001: 创建邮箱账户 (OPEN)
    [简单工单] ST2002: 配置门禁卡 (OPEN)
    [简单工单] ST2003: 分配工位及办公用品 (OPEN)
 [复合工单] T2002: 办公室搬迁IT支持 (OPEN)
    [简单工单] ST2004: 网络布线 (OPEN)
    [简单工单] ST2005: 电话系统迁移 (OPEN)

--- 简单工单处理 ---

开始处理简单工单: 重置用户密码 (ID: T1001)
一线支持人员 王一线 正在处理工单: 重置用户密码
工单 T1001 状态更新为: COMPLETED

开始处理简单工单: 办公室打印机故障 (ID: T1002)
一线支持人员 王一线 正在处理工单: 办公室打印机故障
工单 T1002 状态更新为: COMPLETED

开始处理简单工单: 服务器宕机处理 (ID: T1003)
一线支持人员 王一线 无法处理, 转交二线支持
二线技术支持 李二线 正在处理工单: 服务器宕机处理
工单 T1003 状态更新为: COMPLETED

开始处理简单工单: 采购服务器预算审批 (ID: T1004)
一线支持人员 王一线 无法处理, 转交二线支持
二线技术支持 李二线无法处理, 转交IT经理
IT经理 张经理 正在处理工单: 采购服务器预算审批
工单 T1004 状态更新为: COMPLETED

--- 复杂工单处理 ---

开始处理复合工单: 新员工入职准备 (ID: T2001)

开始处理简单工单: 创建邮箱账户 (ID: ST2001)
一线支持人员 王一线 正在处理工单: 创建邮箱账户
工单 ST2001 状态更新为: COMPLETED

开始处理简单工单: 配置门禁卡 (ID: ST2002)
一线支持人员 王一线 正在处理工单: 配置门禁卡
工单 ST2002 状态更新为: COMPLETED

开始处理简单工单: 分配工位及办公用品 (ID: ST2003)
一线支持人员 王一线 正在处理工单: 分配工位及办公用品
工单 ST2003 状态更新为: COMPLETED
工单 T2001 状态更新为: COMPLETED
复合工单 T2001 所有子工单已完成

开始处理复合工单: 办公室搬迁IT支持 (ID: T2002)

开始处理简单工单: 网络布线 (ID: ST2004)
一线支持人员 王一线 无法处理, 转交二线支持
二线技术支持 李二线 正在处理工单: 网络布线
工单 ST2004 状态更新为: COMPLETED

开始处理简单工单: 电话系统迁移 (ID: ST2005)
一线支持人员 王一线 正在处理工单: 电话系统迁移
工单 ST2005 状态更新为: COMPLETED
工单 T2002 状态更新为: COMPLETED
复合工单 T2002 所有子工单已完成

Process finished with exit code 0

组合优势

  • 组合模式专注于工单组织的表示和管理, 责任链模式专注于工单处理流程的流转。
  • 工单本身无需直到具体的处理逻辑, 只知道将自己提交给一个处理者。处理者也无需知道工单的具体结构, 只知道处理它能处理的工单类型, 或传递给下一个处理者。
相关推荐
Rust研习社2 小时前
组合真的优于继承吗?为什么 Rust 和 Go 都拥抱组合舍弃继承?
后端·rust·编程语言
IT_陈寒2 小时前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
CaffeinePro3 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax3 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH3 小时前
Koa和Express的区别
后端
MariaH3 小时前
Koa框架的使用
后端
luckdewei4 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某6 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy6 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom6 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github