TypeScript设计模式:复合模式

复合模式(Composite Pattern)是一种结构型设计模式,用于将对象组织成树形结构,以表示"部分-整体"的层次关系。客户端可以统一处理单个对象和组合对象,简化操作。

设计模式原理

复合模式通过统一的接口处理单个节点(如文件)和组合节点(如文件夹),客户端无需区分节点类型即可操作整个树形结构。在知识库系统中,复合模式适合表示文件和文件夹的嵌套结构,通过 JSON 配置动态构建树,确保一致性和灵活性。

复合模式的结构

  1. Component(抽象组件接口) :定义文件和文件夹的公共接口,如获取内容、详情、新增、删除和移动。
  2. Leaf(叶子节点) :实现组件接口,表示无子节点的单一文件。
  3. Composite(组合节点) :实现组件接口,管理子节点(如文件夹中的文件或子文件夹)并递归操作。
  4. Client(客户端) :提供 JSON 数组配置,使用组件接口操作树。

优点

  • 一致性:客户端通过统一接口操作文件和文件夹。
  • 层次性:支持树形结构,适合复杂的文件系统嵌套。
  • 可扩展性:易于添加新节点类型(如新文件格式)。

TypeScript 实现示例

以下示例实现一个知识库文件系统:根文件夹包含文档文件和子文件夹,子文件夹中包含其他文件。客户端提供 JSON 数组定义树形结构,知识库引擎使用复合模式管理文件和文件夹。

typescript 复制代码
// 节点类型枚举
enum NodeType {
    FILE = 'file',
    FOLDER = 'folder'
}

// JSON 节点配置接口
interface NodeConfigJson {
    type: NodeType;
    params: {
        id: string;
        name: string;
        [key: string]: any;
    };
    children?: NodeConfigJson[];
}

// 抽象组件接口:知识库节点
interface KnowledgeBaseNode {
    getContent(): string;
    getDetails(): string;
    addChild(child: KnowledgeBaseNode): void;
    removeChild(child: KnowledgeBaseNode): void;
    moveTo(newParent: KnowledgeBaseNode): void;
}

// 叶子节点:文件节点
class FileNode implements KnowledgeBaseNode {
    constructor(private id: string, private params: { name: string; content?: string }) {}

    getContent(): string {
        return `文件内容(ID: ${this.id}):${this.params.content || '空文件'}`;
    }

    getDetails(): string {
        return `详情:文件节点,ID=${this.id},名称=${this.params.name}`;
    }

    addChild(child: KnowledgeBaseNode): void {
        // 文件不能添加子节点
        console.warn(`文件节点(ID: ${this.id})无法添加子节点`);
    }

    removeChild(child: KnowledgeBaseNode): void {
        // 文件无子节点,无需删除
        console.warn(`文件节点(ID: ${this.id})无子节点可删除`);
    }

    moveTo(newParent: KnowledgeBaseNode): void {
        console.log(`文件(ID: ${this.id})已移动到新位置`);
        // 文件移动逻辑:从旧父节点移除(如果有),添加到新父节点
        // 这里简化假设文件无父节点引用,实际可添加 parent 属性
    }
}

// 组合节点:文件夹节点
class FolderNode implements KnowledgeBaseNode {
    private children: KnowledgeBaseNode[] = [];
    private parent?: FolderNode; // 可选:跟踪父节点以支持移动

    constructor(private id: string, private params: { name: string }) {}

    addChild(child: KnowledgeBaseNode): void {
        this.children.push(child);
        child['parent'] = this; // 设置父节点(使用类型断言或接口扩展)
    }

    removeChild(child: KnowledgeBaseNode): void {
        const index = this.children.indexOf(child);
        if (index > -1) {
            this.children.splice(index, 1);
            delete child['parent']; // 移除父节点引用
        }
    }

    moveTo(newParent: KnowledgeBaseNode): void {
        if (this.parent) {
            this.parent.removeChild(this);
        }
        if (newParent instanceof FolderNode) {
            newParent.addChild(this);
        }
        console.log(`文件夹(ID: ${this.id})已移动到新父节点`);
    }

    getContent(): string {
        let result = `文件夹(ID: ${this.id},名称: ${this.params.name})包含以下内容:\n`;
        result += this.children.map(child => `  ${child.getContent()}`).join('\n');
        return result;
    }

    getDetails(): string {
        let result = `详情:文件夹节点,ID=${this.id},名称=${this.params.name}\n`;
        result += this.children.map(child => `  ${child.getDetails()}`).join('\n');
        return result;
    }
}

// 工厂类:创建节点
class KnowledgeBaseNodeFactory {
    private nodeMap: { [key in NodeType]?: new (id: string, params: any) => KnowledgeBaseNode } = {
        [NodeType.FILE]: FileNode,
        [NodeType.FOLDER]: FolderNode
    };

    createNode(json: NodeConfigJson): KnowledgeBaseNode {
        const NodeClass = this.nodeMap[json.type];
        if (!NodeClass) throw new Error(`不支持的节点类型:${json.type}`);
        const node = new NodeClass(json.params.id, json.params);

        // 如果有子节点,递归创建
        if (json.children && json.type === NodeType.FOLDER) {
            json.children.forEach(childConfig => {
                const childNode = this.createNode(childConfig);
                (node as FolderNode).addChild(childNode);
            });
        }
        return node;
    }
}

// 客户端代码:处理 JSON 节点数组并演示操作
function processKnowledgeBase(factory: KnowledgeBaseNodeFactory, nodes: NodeConfigJson[]) {
    const rootNodes: KnowledgeBaseNode[] = [];
    nodes.forEach((nodeConfig) => {
        const node = factory.createNode(nodeConfig);
        rootNodes.push(node);
    });

    // 演示统一接口操作的优点
    console.log("=== 初始结构 ===");
    rootNodes.forEach((node, index) => {
        console.log(`节点 ${index + 1} 内容:\n${node.getContent()}`);
        console.log(`节点 ${index + 1} 详情:\n${node.getDetails()}`);
        console.log("");
    });

    // 示例1: 新增节点(统一接口)
    console.log("=== 新增操作 ===");
    const newFile = factory.createNode({
        type: NodeType.FILE,
        params: { id: "FILE004", name: "新文件", content: "新增内容" }
    });
    rootNodes[0].addChild(newFile); // 根文件夹添加新文件
    console.log("新增文件到根文件夹后内容:\n" + rootNodes[0].getContent());

    // 示例2: 删除节点(统一接口)
    console.log("\n=== 删除操作 ===");
    const targetFile = newFile; // 假设删除刚添加的文件
    rootNodes[0].removeChild(targetFile);
    console.log("删除文件后内容:\n" + rootNodes[0].getContent());

    // 示例3: 移动节点(统一接口)
    console.log("\n=== 移动操作 ===");
    const subFolder = (rootNodes[0] as FolderNode).getChildren?.()[1]; // 获取子文件夹
    if (subFolder instanceof FolderNode) {
        // 创建新根文件夹演示移动
        const newRoot = factory.createNode({
            type: NodeType.FOLDER,
            params: { id: "FOLDER003", name: "新根文件夹" }
        });
        subFolder.moveTo(newRoot);
        console.log("移动子文件夹到新根文件夹后:");
        console.log("原根内容:\n" + rootNodes[0].getContent());
        console.log("新根内容:\n" + newRoot.getContent());
    }
}

// 测试代码
function main() {
    // JSON 配置:根文件夹包含文档和子文件夹,子文件夹包含其他文件
    const knowledgeBaseNodes: NodeConfigJson[] = [
        {
            type: NodeType.FOLDER,
            params: { id: "FOLDER001", name: "根文件夹" },
            children: [
                {
                    type: NodeType.FILE,
                    params: { id: "FILE001", name: "欢迎文档", content: "欢迎使用知识库!" }
                },
                {
                    type: NodeType.FOLDER,
                    params: { id: "FOLDER002", name: "子文件夹" },
                    children: [
                        {
                            type: NodeType.FILE,
                            params: { id: "FILE002", name: "说明文档", content: "系统说明" }
                        },
                        {
                            type: NodeType.FILE,
                            params: { id: "FILE003", name: "日志文件" }
                        }
                    ]
                }
            ]
        }
    ];

    console.log("知识库:根文件夹 → 包含文档和子文件夹(子文件夹包含其他文件)");
    const factory = new KnowledgeBaseNodeFactory();
    processKnowledgeBase(factory, knowledgeBaseNodes);
}

main();

运行结果

ini 复制代码
知识库:根文件夹 → 包含文档和子文件夹(子文件夹包含其他文件)
=== 初始结构 ===
节点 1 内容:
文件夹(ID: FOLDER001,名称: 根文件夹)包含以下内容:
  文件内容(ID: FILE001):欢迎使用知识库!
  文件夹(ID: FOLDER002,名称: 子文件夹)包含以下内容:
    文件内容(ID: FILE002):系统说明
    文件内容(ID: FILE003):空文件

节点 1 详情:
详情:文件夹节点,ID=FOLDER001,名称=根文件夹
  详情:文件节点,ID=FILE001,名称=欢迎文档
  详情:文件夹节点,ID=FOLDER002,名称=子文件夹
    详情:文件节点,ID=FILE002,名称=说明文档
    详情:文件节点,ID=FILE003,名称=日志文件

=== 新增操作 ===
文件节点(ID: FILE004)无法添加子节点  // 如果尝试添加到文件
新增文件到根文件夹后内容:
文件夹(ID: FOLDER001,名称: 根文件夹)包含以下内容:
  文件内容(ID: FILE001):欢迎使用知识库!
  文件夹(ID: FOLDER002,名称: 子文件夹)包含以下内容:
    文件内容(ID: FILE002):系统说明
    文件内容(ID: FILE003):空文件
  文件内容(ID: FILE004):新增内容

=== 删除操作 ===
文件节点(ID: FILE004)无子节点可删除  // 如果尝试从文件删除
删除文件后内容:
文件夹(ID: FOLDER001,名称: 根文件夹)包含以下内容:
  文件内容(ID: FILE001):欢迎使用知识库!
  文件夹(ID: FOLDER002,名称: 子文件夹)包含以下内容:
    文件内容(ID: FILE002):系统说明
    文件内容(ID: FILE003):空文件

=== 移动操作 ===
文件夹(ID: FOLDER002)已移动到新父节点
移动子文件夹到新根文件夹后:
原根内容:
文件夹(ID: FOLDER001,名称: 根文件夹)包含以下内容:
  文件内容(ID: FILE001):欢迎使用知识库!
新根内容:
文件夹(ID: FOLDER003,名称: 新根文件夹)包含以下内容:
  文件夹(ID: FOLDER002,名称: 子文件夹)包含以下内容:
    文件内容(ID: FILE002):系统说明
    文件内容(ID: FILE003):空文件

总结

通过知识库文件系统的 JSON 配置和操作示例,复合模式展示了其核心优势:新增、删除和移动等操作的接口在文件和文件夹上保持一致,客户端无需区分节点类型即可统一处理,而具体实现根据节点类型(如文件夹支持子节点操作,文件忽略)灵活调整。这在知识库系统中提升了代码的一致性和可扩展性,简化了文件系统的管理。

相关推荐
涡能增压发动积21 小时前
同样的代码循环 10次正常 循环 100次就抛异常?自定义 Comparator 的 bug 让我丢尽颜面
后端
Wenweno0o21 小时前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
于慨21 小时前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz21 小时前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
swg32132121 小时前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
从前慢丶21 小时前
前端交互规范(Web 端)
前端
tyung21 小时前
一个 main.go 搞定协作白板:你画一笔,全世界都看见
后端·go
gelald21 小时前
SpringBoot - 自动配置原理
java·spring boot·后端
CHU72903521 小时前
便捷约玩,沉浸推理:线上剧本杀APP功能版块设计详解
前端·小程序
GISer_Jing21 小时前
Page-agent MCP结构
前端·人工智能