Skip to content

组合模式

什么是组合模式

在程序中,可能有些事物是由一些子事物所构成的。组合模式就是用小对象来构建出更大的对象,而这些小对象本身也是由更多的小对象构成的,这就说组合模式

回顾宏命令

在上一节的命令模式中,可以回顾一下宏命令:

js
const closeDoor = {
  execute() {
    console.log('关门')
  }
}

const openPc = {
  execute() {
    console.log('开电脑')
  }
}

const loginQQ = {
  execute() {
    console.log('登陆QQ')
  }
}

function MacroCommand() {
  return {
    commandList: [],
    add(command) {
      this.commandList.push(command)
    },
    execute() {
      this.commandList.map((item) => {
        item.execute()
      })
    }
  }
}

const macroCommand = MacroCommand()
macroCommand.add(closeDoor)
macroCommand.add(openPc)
macroCommand.add(loginQQ)

macroCommand.execute()

// 关门
// 开电脑
// 登陆QQ

以宏命令为例,请求从树的顶端往下传递,如果当前处于的是叶对象(普通子命令),那么就会执行相应的操作。但是请求处理的是组合对象(宏命令),那么就会继续遍历它是子节点。

更强大的宏命令

上面例子中,只是完成了关门、开电脑、打开 QQ 这几个简单命令,接下来来实现一个超级宏命令,来实现下面功能:

  • 打开空调
  • 打开电视和音响
  • 关门、开电脑、打开 QQ

下面以 button 来当作这个超级命令的遥控器,代码如下:

html
<button id="btn">超级遥控器</button>
<script>
  const btn = document.getElementById('btn')

  // 定义宏命令
  function MacroCommand() {
    return {
      commandsList: [],
      add(command) {
        this.commandsList.push(command)
      },
      execute() {
        this.commandsList.map((item) => {
          item.execute()
        })
      }
    }
  }

  // 打开空调
  const openAc = {
    execute() {
      console.log('打开空调')
    }
  }

  // 打开电视和打开音响的命令
  const openTv = {
    execute() {
      console.log('打开电视')
    }
  }

  const openSound = {
    execute() {
      console.log('打开音响')
    }
  }

  const macroCommand1 = MacroCommand()
  macroCommand1.add(openTv)
  macroCommand1.add(openSound)

  // 关门、打开电脑、登陆 QQ 的命令
  const closeDoor = {
    execute() {
      console.log('关门')
    }
  }

  const openPc = {
    execute() {
      console.log('打开电脑')
    }
  }

  const loginQQ = {
    execute() {
      console.log('登陆QQ')
    }
  }

  const macroCommand2 = MacroCommand()
  macroCommand2.add(closeDoor)
  macroCommand2.add(openPc)
  macroCommand2.add(loginQQ)

  // 组合成为超级命令
  const macroCommand = MacroCommand()
  macroCommand.add(openAc)
  macroCommand.add(macroCommand1)
  macroCommand.add(macroCommand2)

  document.getElementById('btn').addEventListener('click', () => {
    macroCommand.execute()
  })

  // 打开空调
  // 打开电视
  // 打开音响
  // 关门
  // 打开电脑
  // 登陆QQ
</script>

安全问题

在上面例子中,其实是存在一些安全问题的,因为很多时候并不清楚谁是组合对象谁是叶对象,但是它们在本质上是有区别的。比如试图在叶对象上再继续添加叶对象。解决方案是在叶对象上叶增加 add 方法,来抛出一个异常提示叶对象不能再继续添加,代码如下:

js
function MacroCommand() {
  return {
    commandsList: [],
    add(command) {
      this.commandsList.push(command)
    },
    execute() {
      this.commandsList.map((item) => {
        item.execute()
      })
    }
  }
}

const openAc = {
  execute() {
    console.log('打开空调')
  },
  add() {
    throw new Error('叶对象上不能再继续添加叶对象')
  }
}

const openTv = {
  execute() {
    console.log('打开电视')
  },
  add() {
    throw new Error('叶对象上不能再继续添加叶对象')
  }
}

const macroCommand = MacroCommand()

macroCommand.add(openAc)
openAc.add(openTv)

扫描文件夹

文件夹是非常符合组合模式来进行描述,下面是一个文件夹相关的例子:

js
// 文件夹类
class Folder {
  constructor(name) {
    this.name = name
    this.files = []
  }
  add(file) {
    this.files.push(file)
  }
  scan() {
    console.log(`开始扫描文件夹:${this.name}`)
    this.files.map((item) => {
      item.scan()
    })
  }
}

// 文件类
class File {
  constructor(name) {
    this.name = name
  }
  add() {
    throw new Error(' 文件下面不能添加文件')
  }
  scan() {
    console.log(`开始扫描文件夹:${this.name}`)
  }
}

// 创建文件夹和文件对象,让它们进行组合
const folder = new Folder('学习资料')
const folder1 = new Folder('js 学习资料')
const folder2 = new Folder('css 学习资料')

const file1 = new File('vue.js 设计与实现')
const file2 = new File('学习 JavaScript 数据结构与算法')
const file3 = new File('css 选择器世界')

folder1.add(file1)
folder1.add(file2)
folder2.add(file3)

folder.add(folder1)
folder.add(folder2)

folder.scan()

// 开始扫描文件夹:学习资料
// 开始扫描文件夹:js 学习资料
// 开始扫描文件夹:vue.js 设计与实现
// 开始扫描文件夹:学习 JavaScript 数据结构与算法
// 开始扫描文件夹:css 学习资料
// 开始扫描文件夹:css 选择器世界

引用父对象

上面演示的文件夹例子中,有可以是要有删除文件的操作,有时候需要让请求从子节点往父节点上冒泡传递的。当我们需要删除某个文件的时候,实际上是从这个文件所在的上层文件夹中删除该文件的。

下面来改写一下上面文件的例子,使扫描之前,我们可以移除一个具体的文件。

js
// 文件夹类
class Folder {
  constructor(name) {
    this.name = name
    this.files = []
    this.parent = null // 增加 parent 属性
  }
  add(file) {
    file.parent = this
    this.files.push(file)
  }
  scan() {
    console.log(`开始扫描文件夹:${this.name}`)
    this.files.map((item) => {
      item.scan()
    })
  }
  // 新增移除方法
  remove() {
    if (!this.parent) {
      return
    }
    this.parent.files.map((item, index) => {
      if (item === this) {
        this.parent.files.splice(index, 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
    }
    this.parent.files.map((item, index) => {
      if (item === this) {
        this.parent.files.splice(index, 1)
      }
    })
  }
}

const folder = new Folder('学习资料')
const folder1 = new Folder('js 学习资料')
const folder2 = new Folder('css 学习资料')

const file1 = new File('vue.js 设计与实现')
const file2 = new File('学习 JavaScript 数据结构与算法')
const file3 = new File('css 选择器世界')

folder1.add(file1)
folder1.add(file2)
folder2.add(file3)

folder.add(folder1)
folder.add(folder2)

folder2.remove()
folder.scan()

// 开始扫描文件夹:学习资料
// 开始扫描文件夹:js 学习资料
// 开始扫描文件夹:vue.js 设计与实现
// 开始扫描文件夹:学习 JavaScript 数据结构与算法