Skip to content

组合模式(Composite)

组合模式就是用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的 “孙对象” 构成

组合模式将对象组合成树形结构,以表示 “部分-整体” 的层次结构

优点

  • 通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。
  • 方便描述对象 部分-整体层次结构。

在组合模式中,请求在树中自上而下的传递,客户只需要关系最顶端的组合对象,当发生请求便会沿着树往下传递,依次到达所有叶对象。

组合模式


在 命令模式 中我有提过宏命令,宏命令是命令模式和组合模式的组合体。在那个例子中,各条命令就是叶对象,当我执行电影模式命令时(组合对象),就会依次传达到所有叶对象。

注意:看起来很像代理,但并非是真正的代理,组合对象只负责传递请求,而目的不在于控制对叶对象的访问。

现在来实现一个更加复杂的宏命令,如:在智能家居中,我设置了一条到家命令,包括打开空调,打开电视和音响,打开浴室灯和打开电热水器。

分析:打开浴室灯和打开电热水器属于洗澡命令,打开电视和音响属于看电视命令,组合在一起则有三条命令:打开空调、洗澡命令、看电视命令。

html
<button id="btn">到家模式</button>

  <script>
    const MacroCommand = function () {
      return {
        commandsList: [],
        add: function (command) {
          this.commandsList.push(command)
        },
        execute: function () {
          for (let i = 0, command; command = this.commandsList[i++];) {
            command.execute()
          }
        }
      }
    }

    const openAcCommand = {
      execute: function () {
        console.log('打开空调');
      }
    }
    // 看电视命令
    const openTvCommand = {
      execute: function () {
        console.log('打开电视')
      }
    }

    const openSoundCommand = {
      execute: function () {
        console.log('打开音响');
      }
    }

    const watchTvCommand = MacroCommand()
    watchTvCommand.add(openTvCommand)
    watchTvCommand.add(openSoundCommand)
    // 洗澡命令
    const openLightsCommand = {
      execute: function () {
        console.log('打开浴室灯')
      }
    }

    const openWaterHeaterCommand = {
      execute: function () {
        console.log('打开电热水器')
      }
    }

    const washCommand = MacroCommand()
    washCommand.add(openLightsCommand)
    washCommand.add(openWaterHeaterCommand)

    const homeCommand = MacroCommand()
    homeCommand.add(openAcCommand)
    homeCommand.add(watchTvCommand)
    homeCommand.add(washCommand)

    const setCommand = (function (command) {
      document.getElementById('btn').onclick = function () {
        command.execute()
      }
    })(homeCommand)
  </script>
<!-- 输出: -->
<!-- 打开空调 -->
<!-- 打开电视 -->
<!-- 打开音响 -->
<!-- 打开浴室灯 -->
<!-- 打开电热水器 -->

组合模式的透明性使得发起请求的客户不用顾忌树中组合对象和叶对象的区别,但它们有本质上有是区别的。

组合对象可以拥有子节点,叶对象下面就没有子节点

javascript
const MacroCommand = function () {
  return {
    commandsList: [],
    add: function (command) {
      this.commandsList.push(command)
    },
    execute: function () {
      for (let i = 0, command; command = this.commandsList[i++];) {
        command.execute()
      }
    }
  }
}

const openAcCommand = {
  execute: function () {
    console.log('打开空调');
  },
  add: function() {
    throw new Error('叶对象不能添加子节点')
  }
}

const macroCommand = MacroCommand()
macroCommand.add(openAcCommand)
openAcCommand.add(macroCommand) // error

注意

  1. 组合模式不是父子关系。
  2. 对叶对象操作的一致性,即对一组叶对象的操作必须具有一致性。
  3. 双向映射关系。即出现一个叶节点可能是该组合对象下多个组合对象的叶节点。
  4. 用职责链模式提高组合模式性能。

组合对象保存了它下面的子节点的引用,但有时候我们需要在子节点上保存父节点的应用,比如当我们删除某个文件的时候,实际上是从该文件的上层文件夹中删除该文件。

javascript
class Folder {
  constructor(name) {
    this.name = name
    this.parent = null
    this.files = []
  }

  add(file) {
    // 叶节点上保存父节点的引用
    file.parent = this
    this.files.push(file)
  }

  scan() {
    console.log('开始扫描文件夹:' + this.name)
    for (let i = 0, file, files = this.files; file = files[i++];) {
      file.scan()
    }
  }
  // 移除该文件夹
  remove() {
    // 判断是根节点或树外节点
    if (!this.parent) return

    for (let files = this.parent.files, l = files.length - 1; l >= 0; l--) {
      const file = files[l]
      if (file === this) files.splice(l, 1)
    }
  }
}

class File {
  constructor(name) {
    this.name = name
    this.parent = null
  }

  add() {
    throw new Error('不能添加在文件下面')
  }

  scan() {
    console.log('开始扫描文件:', this.name)
  }

  remove() {
    if (this.parent) return

    for (let files = this.parent.files, l = files.length - 1; l >= 0; l--) {
      const file = files[l]
      if (file === this) files.splice(l, 1)
    }
  }
}

const folder1 = new Folder('学习资料')
const folder2 = new Folder('JavaScript')
const folder3 = new Folder('workspace')
const file1 = new File('设计模式')

folder1.add(file1)
folder2.add(folder3)
folder1.add(folder2)

folder3.remove() // 删除文件夹
folder1.scan()
// 输入:
// 开始扫描文件夹:学习资料
// 开始扫描文件: 设计模式
// 开始扫描文件夹:JavaScript

Released under the MIT License.