JavaScript 设计模式之组合模式

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

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

回顾宏命令

在命令模式中提到了宏命令的结构和作用。宏命令对象包含了一组具体的子命令对象,不管是宏命令对象还是子命令对象,都有一个 execute 方法负责执行命令。现在回顾一下命令模式中的宏命令代码:

var closeDoorCommand = {
  execute: function () {
    console.log('关门');
  }
};
var openPcCommand = {
  execute: function () {
    console.log('开电脑');
  }
};
var openQQCommand = {
  execute: function () {
    console.log('登录 QQ');
  }
};

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

var macroCommand = MacroCommand();
macroCommand.add(closeDoorCommand);
macroCommand.add(openPcCommand);
macroCommand.add(openQQCommand);
macroCommand.execute();
复制代码

我们可以看到宏命令中包含了一组子命令,它们组成了一个树形结构,尽管是一颗结构很简单的树。
image.png
其中,macroCommand 被称为组合对象,closeDoorCommandopenPcCommandopenQQCommand 都是叶对象。在 macroCommandexecute 方法里,并不执行真正的操作,而是遍历它所包含的叶对象,把真正的 execute 请求委托给这些叶对象。

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

组合模式的用途

1 表示树形结构

上述例子,可以很明显看出组合模式的一个优点:提供了一种遍历树形结构的方案,通过调用组合对象的 execute 方法,程序会递归调用组合对象下面的叶对象的 execute 方法。组合模式可以非常方便地描述对象部分——整体层次结构。

2 利用对象多态性统一对待组合对象和单个对象

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

请求在树中传递的过程

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

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

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

更强大的宏命令

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

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

首先,在节点中放置一个 button 表示这个超级万能遥控器。

<button id=button>按我</button>
<script>
  var MacroCommand = function () {
    return {
      commandsList: [],
      add: function (command) {
        this.commandsList.push(command);
      },
      execute: function () {
        for (var i = 0, command; command = this.commandsList[i++];) {
          command.execute();
        }
      }
    }
  };

  var openAcCommend = {
    execute: function () {
      console.log('打开空调');
    }
  }

  // 电视和音响一起打开
  var openTvCommand = {
    execute: function () {
      console.log('打开电视');
    }
  }
  var openSoundCommand = {
    execute: function () {
      console.log('打开音响');
    }
  }
  var macroCommand1 = MacroCommand()
  macroCommand1.add(openTvCommand)
  macroCommand1.add(openSoundCommand)

  // 关门、开电脑、登QQ的命令
  var closeDoorCommand = {
    execute: function () {
      console.log('关门');
    }
  };
  var openPcCommand = {
    execute: function () {
      console.log('开电脑');
    }
  };
  var openQQCommand = {
    execute: function () {
      console.log('登录 QQ');
    }
  };
  var macroCommand2 = MacroCommand();
  macroCommand2.add(closeDoorCommand);
  macroCommand2.add(openPcCommand);
  macroCommand2.add(openQQCommand);

  // 所有命令组合成一个超级命令
  var macroCommand = MacroCommand();
  macroCommand.add(openAcCommend)
  macroCommand.add(macroCommand1)
  macroCommand.add(macroCommand2)

  // 给超级遥控器绑定命令
  var setCommand = (function (command) {
    document.getElementById('button').onclick = function () {
      command.execute()
    }
  })(macroCommand)
</script>
复制代码

当按下遥控器的按钮时,所有命令依次被执行,如图:
image.png

透明性带来的安全问题

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

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

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

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

一些值得注意的地方

1 组合模式不是父子关系

组合模式是一种 HAS-A(聚合)的关系,而不是 IS-A。组合对象包含一组叶对象,但是 Leaf 并不是 Composite 的子类。组合对象把请求委托给它包含的所有叶对象,它们能够合作的关键是拥有相同的接口。

为了方便描述,有时候会把上下级对象称为父子节点,但是它们并非真正意义上的父子关系。

2 对叶对象操作的一致性

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

3 双向映射关系

如果叶对象从属的组合对象并不单一,那么就需要给叶对象和组合对象建立双向映射关系,一个简单的办法就是给两方都增加集合来保存对方的引用,但是这种相互间的引用相当复杂,而且对象之间产生了过多的耦合性,修改或者删除一个对象都变得困难,此时,我们可以引入中介者模式来管理这些对象。

用职责链模式提高组合模式性能

在组合模式中,如果树的结构比较复杂,节点数量很多,在遍历树的过程中,性能方面也许表现得不够理想。在实际操作中避免遍历整棵树,有一种现成的方案是借助职责链模式。职责链模式一般需要我们手动去设置链条,但在组合模式中,父对象和子对象之间实际上形成了天然的职责链。让请求顺着链条从父对象往子对象传递,或者是反过来从子对象往父对象传递,直到遇到可以处理该请求的对象为止,这也是职责链模式的经典运用场景之一。

何时使用组合模式

1 表示对象的部分——整体层次结构

组合模式可以方便地构造一棵树来表示对象的部分——整体结构。特别是我们在开发期间不确定这棵树到底存在多少层次的时候。在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便,并且符合开放——封闭原则。

2 客户希望统一对待树中的所有对象

组合模式使客户可以忽略组合对象和叶对象的区别,客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就不用写一堆 if、else 语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情,这是组合模式最重要的能力。

小结

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

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

最后说一句

如果这篇文章对您有所帮助,或者有所启发的话,帮忙点赞关注一下,您的支持是我坚持写作最大的动力,多谢支持。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享