Js中的组合模式
定义
是一种将对象组合成树状结构的层次结构模式,用来表示 整体-部分 的关系,使用用户对单个对象和组合对象具有一致的访问性。
详细描述
组合模式是一个树形结构,里面的数据可以是单个对象,也可以n个单个对象组合起来的组合对象。
组合模式主要是用来表示 部分-整体的关系,也就是说组合模式下对于调用者角度来看调用一个对象和调用组合对象没有什么区别,将复杂的组合对象用处理单个对象的方式就可以完成,解耦了客户程序与复杂组合对象的解耦。
因为单个对象就可以表现组合对象,所以要求组合模式中的单个对象和组合对象必须暴露出相同的方法让客户来调用。
示例图
如上图所示当用户需要调用整个对象时,此时因为单个对象和组合对象暴露的调用方法一致,所以用户只需要调用一次最上层的组合对象,组合对象就会将调用逐级往下传递并执行调用,这样用户就可以非常轻松的处理完复杂的组合对象数据。
应用场景
用于想表示部分-整体层次结构,用户可以忽略单个对象与组合对象的不同。
如: 树形结构菜单、文件,文件夹的处理等。
代码实例
宏命令改写
在命令模式中我们写过宏命令的代码(命令模式),此时只需要拿来将命令修改为同样的名字就完成了一个最简单的组合模式:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div>
<button id="button1">button1</button>
</div>
</body>
<script>
const button1 = document.getElementById('button1');
const setCommand = (button, command) => {
// 调用最顶层对象
button.onclick = () => command.execute();
};
const refresh = {
execute() {
console.log('刷新功能');
},
};
const add = {
execute() {
console.log('添加功能');
},
};
const del = {
execute() {
console.log('删除功能');
},
};
const MenuBarCommand = (receiver) => {
return {
commandsList: [],
// 暴露供用户调用的统一接口
execute() {
// 调用逐级传递执行
this.commandsList.forEach((obj) => obj.execute());
},
// 收集子对象
add(obj) {
this.commandsList.push(obj);
},
};
};
const macroCommand = MenuBarCommand();
macroCommand.add(add);
macroCommand.add(del);
macroCommand.add(refresh);
// 绑定组合对象
setCommand(button1, macroCommand);
// 点击按钮后打印结果
// 添加功能
// 删除功能
// 刷新功能
</script>
</html>
上述代码中组合对象macroCommand 由三个单独对象组合而成,他们都暴漏出相同执行命令的方法execute ,调用者只调用最顶层的组合对象macroCommand就相当于调用了整个组合对象。上面只是最简单的对象组合下面我们看下更复杂对象的组合实例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div>
<button id="button1">button1</button>
</div>
</body>
<script>
const button1 = document.getElementById('button1');
const setCommand = (button, command) => {
// 调用最顶层对象
button.onclick = () => command.execute();
};
const MacroCommand = (receiver) => {
return {
commandsList: [],
execute() {
this.commandsList.forEach((obj) => obj.execute());
},
add(obj) {
this.commandsList.push(obj);
},
};
};
// 定义单个对象
const refresh = {
execute() {
console.log('刷新功能');
},
};
const add = {
execute() {
console.log('添加功能');
},
};
const del = {
execute() {
console.log('删除功能');
},
};
const jump = {
execute() {
console.log('跳转功能');
},
};
const prints = {
execute() {
console.log('打印功能');
},
};
const back = {
execute() {
console.log('返回功能');
},
};
// 定义组合对象
const macroCommand1 = MacroCommand();
macroCommand1.add(refresh);
macroCommand1.add(add);
// 定义组合对象
const macroCommand2 = MacroCommand();
macroCommand2.add(del);
macroCommand2.add(jump);
macroCommand2.add(prints
);
// 定义组合对象
const macroCommand = MacroCommand();
macroCommand.add(macroCommand1);
macroCommand.add(macroCommand2);
macroCommand.add(back);
// 绑定组合对象
setCommand(button1, macroCommand);
// 点击按钮调用结果
// 刷新功能
// 添加功能
// 删除功能
// 跳转功能
// 打印功能
// 返回功能
</script>
</html>
上述组合对象示例图:
上述代码就是更加复杂一些的组合对象macroCommand 它的组成不但包含单个对象还包含了组合对象back 还包含了组合对象macroCommand1和macroCommand2,但是对于调用者来着没有任何区别只需要调用macroCommand对象的execute方法就可以轻松的调用整个组合对象中的每个执行。此时如果需要添加新的执行和组合对象只需要暴露出和原结构相同的execute方法即可(参考在下面文件夹实例),非常易于维护和扩展。
文件处理
文件夹和文件之间的关系,非常适合用组合模式来描述。文件夹里既可以包含文件,又可以 包含其他文件夹,最终可能组合成一棵树。下面看下扫描文件的实例:
class Folder {
constructor(name) {
this.name = name;
this.list = [];
}
// 扫描
scean() {
console.log('开始扫描文件夹 -->' + this.name);
this.list.forEach((file) => file.scean());
}
// 添加
add(file) {
this.list.push(file);
}
}
class File {
constructor(name) {
this.name = name;
}
scean() {
console.log('开始扫描文件 -->' + this.name);
}
// 防止单个对象误操作,添加提示解决透明性带来的安全问题
add() {
throw new Error('文件下面不能再添加文件');
}
}
// 定义文件夹
const folder = new Folder('学习资料');
const folder1 = new Folder('JavaScript');
const folder2 = new Folder('jQuery');
// 定义文件
const file1 = new File('JavaScript 设计模式与开发实践');
const file2 = new File('精通 jQuery');
const file3 = new File('重构与模式');
folder1.add(file1);
folder2.add(file2);
folder.add(folder1);
folder.add(folder2);
folder.add(file3);
// 下面将其他文件夹和其下面的文件移动到添加好的文件夹folder中(扩展组合对象中的数据)
const folder3 = new Folder('Nodejs');
const file4 = new File('深入浅出 Node.js');
folder3.add(file4);
const file5 = new File('JavaScript 语言精髓与编程实践');
folder.add(folder3);
folder.add(file5);
// 最后开始扫描整个文件目录
folder.scean();
// 扫描结果
// 开始扫描文件夹 -->学习资料
// 开始扫描文件夹 -->JavaScript
// 开始扫描文件 -->JavaScript 设计模式与开发实践
// 开始扫描文件夹 -->jQuery
// 开始扫描文件 -->精通 jQuery
// 开始扫描文件 -->重构与模式
// 开始扫描文件夹 -->Nodejs
// 开始扫描文件 -->深入浅出 Node.js
// 开始扫描文件 -->JavaScript 语言精髓与编程实践
上述代码更加清晰的展现了组合模式中 部分-整体原则,用户不用区分文件和文件夹的区别,直接调用scean方法就可以完成整个文件目录的扫描。 而当我们想要添加文件数据的时候,只需要直接调用原来的方法添加,不需要改动代码就可以完成。
总结
通过上述实例我们可以看到组合模式表示部分-整体的特性,所以我们在维护这些特性的时候就必须要保持单个对象与组合对象的一致性。
正因为部分与整体的一致性,所以组合模式具有调用非常简单,数据扩展更加灵活的特点。
同时在保持灵活,简单优点的时候因为单个对象和组合对象几乎一样,会使代码不易于阅读,并且由于创建了太多的对象可能会增加系统开销问题。