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

相关推荐
zjjuejin1 小时前
Maven依赖管理艺术
后端·maven
我是天龙_绍1 小时前
CSS/JS/图片全挂了,部署后页面白屏/资源加载失败?这两个配置项坑了多少人!
前端
我的小月月1 小时前
SQLFE:网页版数据库(VUE3+Node.js)
前端·后端
小高0071 小时前
🌐ES6 这 8 个隐藏外挂,知道 3 个算我输!
前端·javascript·面试
汤姆Tom1 小时前
Node.js 版本管理、NPM 命令、与 NVM 完全指南
前端·npm·node.js
Alan521591 小时前
Java 后端实现基于 JWT 的用户认证和权限校验(含代码讲解)
前端·后端
RoyLin2 小时前
TypeScript设计模式:策略模式
前端·后端·typescript
brzhang2 小时前
为什么说低代码谎言的破灭,是AI原生开发的起点?
前端·后端·架构
得物技术2 小时前
破解gh-ost变更导致MySQL表膨胀之谜|得物技术
数据库·后端·mysql