设计模式之组合模式

446 阅读6分钟

作者:葉@毛豆前端

组合模式

一、定义

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

在定义中提到了“部分-整体”、“单个对象”、“组合对象”这几个关键词,因此掌握组合模式的重点是要理解清楚 “部分/整体” 还有 ”单个对象“ 与 "组合对象" 的含义

二、作用

组合模式的作用即定义描述的那样,有两个作用:

  1. 将对象组合成树形结构,以表示“部分-整体”的层次结构
  2. 通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性

针对以上的两点,下面做一个详细说明,首先看一段代码:这是一个模拟电脑开机,运行应用程序的简单宏命令例子

let runWeChat = {
    run : function() {
        console.log("wechat已经启动")
    }
}

let runQQ = {
    run : function() {
        console.log("QQ已经启动")
    }
}

let runChrome = {
    run : function() {
        console.log("Google Chrome已经启动")
    }
}

let applicationCommand = function () {
    return {
        commandLists : [],
        addCommand: function (command) {
            this.commandLists.push(command)
        },
        run: function () {
            this.commandLists.map((item, index) => {
                item.run()
            })
        }
    }
}

let ac = applicationCommand ()

ac.addCommand(runWeChat)

ac.addCommand(runQQ)

ac.addCommand(runChrome)

ac.run()

  • 这样树形遍历的结构,通过调用组合对象的run方法,程序便会递归调用组合对象下面的子对象的run方法,因此我们只要按下电脑的开机键,微信,qq,谷歌浏览器这些程序就会被顺序运行。这样的方式很简洁的描述出了部分-整体的结构
  • 在这个组合模式中,利用对象的多态性表现,我们可以不关心组合对象和单个对象的区别,使用时统一使用组合结构中的所有对象,而不需要关心它是组合对象还是单个对象

这样的方式在实际的开发中可以带来很大的便利性。当我们在开机程序中添加命令时,无需关心这个是宏命令还是子命令,只关心这个命令是有可以执行的run方法,那么这个命令就可以被添加。执行的差异是在代码里体现的,对客户无感。

仍以上面的宏命令为例,请求在树最顶端的对象往下传递,如果此时处理请求的是普通子命令(叶子对象),这个时候叶子对象自己就会对请求做出处理;若此时处理请求的宏命令(组合对象),组合对象则遍历它的子对象,将请求传递下去。上面的例子只是简单的组合对象下只有叶子对象,叶子对象是树的最小单位,不会再有其他子节点,组合对象下可能还有子节点,如下图:

请求是由上至下沿着树进行传递,直到叶子对象,作为使用者,只需要关心树最顶层的组合对象,只要请求这个组合对象,请求便会沿着树往下传递,顺次到达所有的叶子对象

三、组合模式例子-员工部门关系

在公司里,必然存在员工的部门以及从属关系(这里只考虑每个人只属于一个部门),公司分为众多的部门,每个部门会有一个主管,带领下面的员工,这个员工可能只是最末端的一个职位,也有可能在他手下还有一波兄弟,每个部门是一个组合对象,部门下面的小组还是一个组合对象,这样就给构成了组合的模式,接下来我们来看下例子:

let leaderStaff = function (name, sex, apartment) {
    this.name = name;
    this.sex = sex;
    this.apartment = apartment;
    this.children = [];
}
leaderStaff.prototype.add = function (child) {
    this.children.push(child)
}
leaderStaff.prototype.doCount = function () {
    console.log("leader", this.name +"--"+ this.sex +"--"+ this.apartment)
    this.children.map((child, index) => {
        child.doCount()
    })
}   
let staff = function(name, sex, apartment) {
    this.name = name;
    this.sex = sex;
    this.apartment = apartment;
}
staff.prototype.add = function() {
    throw new Error("我还只是个孩子!!->_->")
}
staff.prototype.doCount = function () {
    console.log("普通员工",this.name +"--"+ this.sex +"--"+ this.apartment)
}
let leaderStaff1 = new leaderStaff('大旺', '男', 'A')
let leaderStaff2 = new leaderStaff('大张', '男', 'B')
let leaderStaff3 = new leaderStaff('大李', '男', 'C')
let leaderStaff4 = new leaderStaff('大无', '女', 'D')
let staff5 = new staff('小马', '', '0') 
let staff1 = new staff('小黑', '男', 'd')
let staff2 = new staff('小撒', '女', 'e')
let staff3 = new staff('小周', '女', 'f')
let staff4 = new staff('小郑', '男', 'g')

leaderStaff1.add(leaderStaff3);
leaderStaff1.add(leaderStaff2);
leaderStaff1.add(leaderStaff4)
leaderStaff1.add(staff1)

leaderStaff3.add(staff4)
leaderStaff3.add(staff3)
leaderStaff4.add(staff2)
// staff3.add(staff5)

leaderStaff1.doCount()

执行结果如图:

这样看起来似乎树形结构不是很明显,所以我们再看下中间执行的数据:

这里我们把拥有下级(子元素)的都统称为老板(leaderStaff),没有下级(子元素)的都叫做员工(staff),员工下面在没有子元素,否则抛出错误。他们都有doCount这个方法。很明显leaderStaff1(大旺)有四个子元素,其中大李又有两个子元素,以及大无有一个子元素,这些都是被称作组合对象,使用的人也不需要分辨他是孩子对象还是祖先元素。

这里我们改变树的结构,增加新的数据,却不用修改原来的代码,这是可取的,符合开放-封闭原则

四、注意事项

我们现在了解了组合模式,还要说下在使用组合模式的实收,有一些值得我们注意的地方:

  1. 组合模式不是父子关系 组合模式的树形结构容易让人误以为组合对象和叶子对象之间是父子关系,但并不是这样的。组合模式是一种HAS-A(聚合)的关系,而不是IS-A,组合对象把请求委托给它所包的所有叶对象,它们能够合作的关键是拥有相同是的接口

  2. 对叶对象操作的一致性 组合模式除了要求组合对象和也对象拥有相同的接口之外,还有一个必要条件,就是对一组也对象的操作必须具有一致性

  3. 双向映射关系 上面的例子中,公司给员工发送通知的步骤是从公司到各个部分,再到小组,最后才到每个员工的邮箱里,这便是一个组合模式的例子,但是存在特殊情况,一个员工同属于多个部门的时候就没有了严格意义上的层次结构,在这种情况下便不再适用组合模式,否则该员工很可能收到两份邮件

  4. 用职责链模式提高组合模式性能 一些情况下,生成树的结构十分复杂,节点数量众多,在遍历的过程中,消耗性能。职责链模式就是在遍历的过程中,只让请求顺着联调从父元素往子元素上传递,或子元素往父元素身上船体,知道遇到能处理请求的对象未知,避免浪费

五、总结

虽然多数时候组,合模式带给我们便利,让我们可以使用树形的方式去创建对象结构,还可以忽略组合对象和单个对象之间的差别,使用一致的方式处理。但是,它的缺点也需要注意:他们的区别只有在运行的时候才会体现出来。