复合模式(Composite Pattern)是一种结构型设计模式,用于将对象组织成树形结构,以表示"部分-整体"的层次关系。客户端可以统一处理单个对象和组合对象,简化操作。
设计模式原理
复合模式通过统一的接口处理单个节点(如文件)和组合节点(如文件夹),客户端无需区分节点类型即可操作整个树形结构。在知识库系统中,复合模式适合表示文件和文件夹的嵌套结构,通过 JSON 配置动态构建树,确保一致性和灵活性。
复合模式的结构
- Component(抽象组件接口) :定义文件和文件夹的公共接口,如获取内容、详情、新增、删除和移动。
- Leaf(叶子节点) :实现组件接口,表示无子节点的单一文件。
- Composite(组合节点) :实现组件接口,管理子节点(如文件夹中的文件或子文件夹)并递归操作。
- 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 配置和操作示例,复合模式展示了其核心优势:新增、删除和移动等操作的接口在文件和文件夹上保持一致,客户端无需区分节点类型即可统一处理,而具体实现根据节点类型(如文件夹支持子节点操作,文件忽略)灵活调整。这在知识库系统中提升了代码的一致性和可扩展性,简化了文件系统的管理。