组合模式

在现实生活中,存在很多“部分-整体”的关系,例如,大学中的部门与学院、总公司中的部门与分公司、学习用品中的书与书包、生活用品中的衣服与衣柜、以及厨房中的锅碗瓢盆等。在软件开发中也是这样,例如,文件系统中的文件与文件夹、窗体程序中的简单控件与容器控件等。对这些简单对象与复合对象的处理,如果用组合模式来实现会很方便。


组合模式的定义与特点

组合(Composite Pattern)模式的定义:有时又叫作整体-部分(Part-Whole)模式,它是一种将对象组合成树状的层次结构的模式,用来表示“整体-部分”的关系,使用户对单个对象和组合对象具有一致的访问性,属于结构型设计模式。
组合模式一般用来描述整体与部分的关系,它将对象组织到树形结构中,顶层的节点被称为根节点,根节点下面可以包含树枝节点和叶子节点,树枝节点下面又可以包含树枝节点和叶子节点,树形结构图如下。


由上图可以看出,其实根节点和树枝节点本质上属于同一种数据类型,可以作为容器使用;而叶子节点与树枝节点在语义上不属于用一种类型。但是在组合模式中,会把树枝节点和叶子节点看作属于同一种数据类型(用统一接口定义),让它们具备一致行为。
这样,在组合模式中,整个树形结构中的对象都属于同一种类型,带来的好处就是用户不需要辨别是树枝节点还是叶子节点,可以直接进行操作,给用户的使用带来极大的便利。
组合模式的主要优点有:

  1. 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;
  2. 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”;

其主要缺点是:

  1. 设计较复杂,客户端需要花更多时间理清类之间的层次关系;
  2. 不容易限制容器中的构件;
  3. 不容易用继承的方法来增加构件的新功能;


基本知识

组合模式也是结构型设计模式的一种,它主要体现了整体与部分的诶关系,其典型的应用就是树形结构。组合是一组对象,其中的对象可能包含一个其他对象,也可能包含一组其他对象

组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性

在使用组合模式的使用要注意以下两点

  • 组合中既要能包含个体,也要能包含其他组合

  • 要抽象出对象和组合的公共特性

组合模式主要有三个角色

  • 抽象组件(Component):抽象类,主要定义了参与组合的对象的公共接口

  • 子对象(Leaf):组成组合对象的最基本对象

  • 组合对象(Composite):由子对象组合起来的复杂对象

理解组合模式的关键是要理解组合模式对单个对象和组合对象使用的一致性,如下解析组合模式的实现加深理解

 

核心

可以用树形结构来表示这种“部分- 整体”的层次结构

调用组合对象的execute方法,程序会递归调用组合对象 下面的叶对象的execute方法,在后续的实现中体现


但要注意的是,组合模式不是父子关系,它是一种HAS-A(聚合)的关系,将请求委托给 它所包含的所有叶对象,基于这种委托,就需要保证组合对象和叶对象拥有相同的接口

此外,也要保证用一致的方式对待 列表中的每个叶对象,即叶对象属于同一类,不需要过多特殊的额外操作

 

实现

宏命令对象包含了一组具体的子命令对象,不管是宏命令对象,还是子命令对象,都有 一个execute方法负责执行命令

现在我们来造一个“万能遥控器”

// 新建一个关门的命令
const closeDoorCommand = {
execute:function(){
console.log('关门' )
}
}
// 新建一个开电脑的命令
const openPcCommand = {
execute:function(){
console.log('开电脑' )
}
};
// 登陆QQ的命令
const openQQCommand = {
execute:function(){
console.log('登录QQ' )
}
};

// 创建一个宏命令
const MacroCommand =function(){
return {
// 宏命令的子命令列表
commandsList: [],
// 添加命令到子命令列表
add:function( command ){
this.commandsList.push( command )
},
// 依次执行子命令列表里面的命令
execute:function(){
for (var i = 0, command; command =this.commandsList[ i++ ]; ){
command.execute()
}
}
}
}

const macroCommand = MacroCommand()
macroCommand.add( closeDoorCommand )
macroCommand.add( openPcCommand )
macroCommand.add( openQQCommand )
macroCommand.execute()

通过观察这段代码,我们很容易发现,宏命令中包含了一组子命令,它们组成了一个树形结构,这里是一棵结构非常简单的树

其中,marcoCommand被称为组合对象,closeDoorCommand、openPcCommand、openQQCommand都是叶对象。在macroCommand的execute方法里,并不执行真正的操作,而是遍历它所包含的叶对象,把真正的execute请求委托给这些叶对象

macroCommand表现得像一个命令,但它实际上只是一组真正命令的“代理”。并非真正的代理,虽然结构上相似,但macroCommand只负责传递请求给叶对象,它的目的不在于控制对叶对象的访问


组合模式的用途

组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性

  • 表示树形结构。组合模式有一个优点:提供了一种遍历树形结构的方案,通过调用组合对象的execute方法,程序会递归调用组合对象下面的叶对象的execute方法,所以我们的万能遥控器只需要一次操作,便能依次完成关 门、打开电脑、登录QQ这几件事情。组合模式可以非常方便地描述对象部分-整体层次结构

  • 利用对象多态性统一对待组合对象和单个对象。利用对象的多态性表现,可以使客户端忽略组合对象和单个对象的不同。在组合模式中,客户将统一地使用组合结构中的所有对象,而不需要关心它究竟是组合对象还是单个对象

这在实际开发中会给客户带来相当大的便利性,当我们往万能遥控器里面添加一个命令的时候,并不关心这个命令是宏命令还是普通子命令。这点对于我们不重要,我们只需要确定它是一个命令,并且这个命令拥有可执行的execute方法,那么这个命令就可以被添加进万能遥控器

当宏命令和普通子命令接收到执行execute方法的请求时,宏命令和普通子命令都会做它们各自认为正确的事情。这些差异是隐藏在客户背后的,在客户看来,这种透明性可以让我们非常自由地扩展这个万能遥控器


更强大的宏命令

目前的“万能遥控器”,包含了关门、开电脑、登录QQ这3个命令。现在我们需要一个“超级万能遥控器”,可以控制家里所有的电器,这个遥控器拥有以下功能

  • 打开空调

  • 打开电视和音响

  • 关门、开电脑、登录QQ

// 创建一个宏命令
const MacroCommand =function(){
return {
// 宏命令的子命令列表
commandsList: [],
// 添加命令到子命令列表
add:function( command ){
this.commandsList.push( command )
},
// 依次执行子命令列表里面的命令
execute:function(){
for (var i = 0, command; command =this.commandsList[ i++ ]; ){
command.execute()
}
}
}
}

<!--打开空调命令-->
const openAcCommand = {
execute:function(){
console.log('打开空调' )
}
}

<!--打开电视和音响-->
const openTvCommand = {
execute:function(){
console.log('打开电视' )
}
}
var openSoundCommand = {
execute:function(){
console.log('打开音响' )
}
}
// 创建一个宏命令
const macroCommand1 = MacroCommand()
// 把打开电视装进这个宏命令里
macroCommand1.add(openTvCommand)
// 把打开音响装进这个宏命令里
macroCommand1.add(openSoundCommand)

<!--关门、打开电脑和打登录QQ的命令-->
const closeDoorCommand = {
execute:function(){
console.log('关门' )
}
}
const openPcCommand = {
execute:function(){
console.log('开电脑' )
}
}
const openQQCommand = {
execute:function(){
console.log('登录QQ' )
}
};
//创建一个宏命令
const macroCommand2 = MacroCommand()
//把关门命令装进这个宏命令里
macroCommand2.add( closeDoorCommand )
//把开电脑命令装进这个宏命令里
macroCommand2.add( openPcCommand )
//把登录QQ命令装进这个宏命令里
macroCommand2.add( openQQCommand )

<!--把各宏命令装进一个超级命令中去-->
const macroCommand = MacroCommand()
macroCommand.add( openAcCommand )
macroCommand.add( macroCommand1 )
macroCommand.add( macroCommand2 )

从以上代码可以看出基本对象可以被组合成更复杂的组合对象,合对象又可以被组合,这样不断递归下去,这棵树的结构可以支持任意多的复杂度。在树最终被构造完成之后,让整颗树最终运转起来的步骤非常简单,只需要调用最上层对象的execute方法。每当对最上层的对象进行一次请求时,实际上是在对整个树进行深度优先的搜索,而创建组合对象的程序员并不关心这些内在的细节,往这棵树里面添加一些新的节点对象是非常容易的事情



链接: https://fly63.com/course/27_1270