「设计模式 JavaScript 描述」组合模式

简介: 「设计模式 JavaScript 描述」组合模式

「设计模式 JavaScript 描述」组合模式

在程序设计中,有一些和“事物是由相似的子事物构成” 类似的思想。「组合模式」就是用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成的。

1. 回顾宏命令

我们在之前命令模式中讲解过宏命令的结构和作用。宏命令对象包含了一组具体的子命令对象,不管是宏命令对象,还是子命令对象,都有一个 execute 方法负责执行命令。现在回顾一下这段安装在万能遥控器上的宏命令代码:

const closeDoorCommand = {
  execute: function () {
    console.log('关门');
  }
};
const openPcCommand = {
  execute: function () {
    console.log('开电脑');
  }
};
const openQQCommand = {
  execute: function () {
    console.log('登录 QQ');
  }
};
const MacroCommand = function () {
  return {
    commandsList: [],
    add: function (command) {
      this.commandsList.push(command);
    },
    execute: function () {
      for (let i = 0; i < this.commandsList.length; i++) {
        this.commandsList[i].execute();
      }
    }
  }
};
const macroCommand = MacroCommand();
macroCommand.add(closeDoorCommand);
macroCommand.add(openPcCommand);
macroCommand.add(openQQCommand);
macroCommand.execute();

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

758f09832c6e347794a58e0b5055ee3a.png

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

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

2. 组合模式的用途

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

  • 表示树形结构。通过回顾上面的例子,我们很容易找到组合模式的一个优点:提供了一 种遍历树形结构的方案,通过调用组合对象的 execute 方法,程序会递归调用组合对象下面的叶对象的 execute 方法,所以我们的万能遥控器只需要一次操作,便能依次完成关门、打开电脑、登录 QQ 这几件事情。组合模式可以非常方便地描述对象部分—整体层次结构。
  • 利用对象多态性统一对待组合对象和单个对象。利用对象的多态性表现,可以使客户端忽略组合对象和单个对象的不同。在组合模式中,客户将统一地使用组合结构中的所有对象,而不需要关心它究竟是组合对象还是单个对象。

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

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

3. 请求在树中传递的过程

在组合模式中,请求在树中传递的过程总是遵循一种逻辑。

以宏命令为例,请求从树最顶端的对象往下传递,如果当前处理请求的对象是叶对象(普通子命令),叶对象自身会对请求作出相应的处理;如果当前处理请求的对象是组合对象(宏命令),组合对象则会遍历它属下的子节点,将请求继续传递给这些子节点。

总而言之,如果子节点是叶对象,叶对象自身会处理这个请求,而如果子节点还是组合对象,请求会继续往下传递。叶对象下面不会再有其他子节点,一个叶对象就是树的这条枝叶的尽头,组合对象下面可能还会有子节点,如下图所示。

77e0e97d01e20922f75fb62a359d9ac4.png

请求从上到下沿着树进行传递,直到树的尽头。作为客户,只需要关心树最顶层的组合对象,客户只需要请求这个组合对象,请求便会沿着树往下传递,依次到达所有的叶对象。

在刚刚的例子中,由于宏命令和子命令组成的树太过简单,我们还不能清楚地看到组合模式带来的好处,如果只是简单地遍历一组子节点,迭代器便能解决所有的问题。接下来我们将创造一个更强大的宏命令,这个宏命令中又包含了另外一些宏命令和普通子命令,看起来是一棵相对较复杂的树。

4. 更强大的宏命令

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

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

首先在节点中放置一个按钮 button 来表示这个超级万能遥控器,超级万能遥控器上安装了一个宏命令,当执行这个宏命令时,会依次遍历执行它所包含的子命令,代码如下:

<body>
  <button id="button">按我</button>
</body>
<script>
  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('打开电视');
    }
  };
  const 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);
  macroCommand2.add(openQQCommand);
  /*********现在把所有的命令组合成一个“超级命令”**********/
  const macroCommand = MacroCommand();
  macroCommand.add(openAcCommand);
  macroCommand.add(macroCommand1);
  macroCommand.add(macroCommand2);
  /*********最后给遥控器绑定“超级命令”**********/
  const setCommand = (function (command) {
    document.getElementById('button').onclick = function () {
      command.execute();
    }
  })(macroCommand);
</script>

当按下遥控器的按钮时,所有命令都将被依次执行,执行结果如下图所示。

60d29b8b2262dd92d79df612c318cc1c.png

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

5. 透明性带来的安全问题

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

组合对象可以拥有子节点,叶对象下面就没有子节点, 所以我们也许会发生一些误操作,比如试图往叶对象中添加子节点。解决方案通常是给叶对象也增加 add 方法,并且在调用这个方法时,抛出一个异常来及时提醒客户,代码如下:

const MacroCommand = function () {
  return {
    commandsList: [],
    add: function (command) {
      this.commandsList.push(command);
    },
    execute: function () {
      for (let i = 0; i < this.commandsList.length; i++) {
        this.commandsList[i].execute();
      }
    }
  }
};
const openTvCommand = {
  execute: function () {
    console.log('打开电视');
  },
  add: function () {
    throw new Error('叶对象不能添加子节点');
  }
};
const macroCommand = MacroCommand();
macroCommand.add(openTvCommand);
openTvCommand.add(macroCommand) // Uncaught Error: 叶对象不能添加子节点

6. 何时使用组合模式

组合模式如果运用得当,可以大大简化客户的代码。一般来说,组合模式适用于以下这两种情况。

  • 表示对象的部分—整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分—整体结构。特别是我们在开发期间不确定这棵树到底存在多少层次的时候。在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便,并且符合开放—封闭原则。
  • 客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和叶对象的区别,客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就不用写一堆 if、else 语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情,这是组合模式最重要的能力。

7. 小结

本文我们了解了组合模式在 JavaScript 开发中的应用。组合模式可以让我们使用树形方式创建对象的结构。我们可以把相同的操作应用在组合对象和单个对象上。在大多数情况下,我们都可以忽略掉组合对象和单个对象之间的差别,从而用一致的方式来处理它们。

然而,组合模式并不是完美的,它可能会产生一个这样的系统:系统中的每个对象看起来都与其他对象差不多。它们的区别只有在运行的时候会才会显现出来,这会使代码难以理解。此外,如果通过组合模式创建了太多的对象,那么这些对象可能会让系统负担不起。


相关文章
|
2天前
|
设计模式 前端开发 JavaScript
【JavaScript 技术专栏】JavaScript 设计模式与实战应用
【4月更文挑战第30天】本文探讨JavaScript设计模式在提升开发效率和代码质量中的关键作用。涵盖单例、工厂、观察者、装饰器和策略模式,并通过实例阐述其在全局状态管理、复杂对象创建、实时数据更新、功能扩展和算法切换的应用。理解并运用这些模式能帮助开发者应对复杂项目,提升前端开发能力。
|
2天前
|
存储 缓存 JavaScript
请描述一种JavaScript内存泄漏的情况,并说明如何避免这种情况的发生。
JavaScript内存泄漏常由闭包引起,导致无用对象滞留内存,影响性能。例如,当一个函数返回访问大型对象的闭包,即使函数执行完,对象仍被闭包引用,无法被垃圾回收。防止泄漏需及时解除引用,注意事件监听器清理,使用WeakMap或WeakSet,定期清理缓存,以及利用性能分析工具检测。
13 2
|
2天前
|
JavaScript 前端开发 算法
描述 JavaScript 中的垃圾回收机制。
描述 JavaScript 中的垃圾回收机制。
23 1
|
2天前
|
移动开发 JavaScript 前端开发
游戏框架 - 描述Phaser、Three.js等JavaScript游戏框架的核心功能和使用场景。
Phaser是开源2D游戏引擎,适合HTML5游戏,内置物理引擎和强大的图形渲染功能,适用于2D游戏,如消消乐。Three.js是基于WebGL的3D库,用于创建和显示3D图形,支持交互和多种3D效果,广泛应用在游戏、可视化等多个领域。两者各有侧重,选择取决于项目需求和图形交互要求。
53 3
|
21小时前
|
设计模式 存储 前端开发
JS的几种设计模式,Web前端基础三剑客学习知识分享,前端零基础开发
JS的几种设计模式,Web前端基础三剑客学习知识分享,前端零基础开发
|
2天前
|
设计模式 Java
【设计模式】文件目录管理是组合模式吗?
【设计模式】文件目录管理是组合模式吗?
7 0
|
2天前
|
设计模式 JavaScript 前端开发
js设计模式-观察者模式与发布/订阅模式
观察者模式和发布/订阅模式是JavaScript中的两种设计模式,用于处理对象间的通信和事件处理。观察者模式中,一个主题对象状态改变会通知所有观察者。实现包括定义主题和观察者对象,以及在主题中添加、删除和通知观察者的功能。发布/订阅模式则引入事件管理器,允许发布者发布事件,订阅者通过订阅接收通知。
|
2天前
|
设计模式 JavaScript 算法
js设计模式-策略模式与代理模式的应用
策略模式和代理模式是JavaScript常用设计模式。策略模式通过封装一系列算法,使它们可互换,让算法独立于客户端,提供灵活的选择。例如,定义不同计算策略并用Context类执行。代理模式则为对象提供代理以控制访问,常用于延迟加载或权限控制。如创建RealSubject和Proxy类,Proxy在调用RealSubject方法前可执行额外操作。这两种模式在复杂业务逻辑中发挥重要作用,根据需求选择合适模式解决问题。
|
2天前
|
设计模式 安全 Java
[设计模式Java实现附plantuml源码~结构型]树形结构的处理——组合模式
[设计模式Java实现附plantuml源码~结构型]树形结构的处理——组合模式
|
2天前
|
设计模式 Go
[设计模式 Go实现] 结构型~组合模式
[设计模式 Go实现] 结构型~组合模式