原文:https://addyosmani.com/resources/essentialjsdesignpatterns/book/#facadepatternjavascript

我们搭建一个 facade 时,我们展示给外界只是一个外表,它的背后可能隐藏着非常不同的现实。这就是我们接下来要讲到的这个模式的命名灵感来源:外观模式。这个模式为更大的代码体提供了一个方便的高级接口,隐藏了它底层的真正的复杂性。可以把它想象是成为其他开发者提供的简化 API,这几乎总是会提高可用性。

虽然一个实现可能支持具有各种行为的方法,但是只有 “外观” 或者这些方法的有限抽象是可以被公开使用。外观模式是一种结构式的设计模式,它在像 jQuery 这类 JavaScript 库中很常见,虽然它的实现是由很多不同行为的方法支持的,但是只有 “外观” 或者这些方法的有限的抽象是可以被公众所使用。

这就使我们可以直接同外观进行交互,和而不是它背后的子系统。每当我们使用 jQuery 的 $(el).css 或者 $(el).animate() 方法时,我们实际使用的就是一个外观 - 一个更简单的公共接口,来让我们避免手动调用 jQuery 核心中内部方法,以使某些行为正常工作。这也避免了手动与 DOM API 交互和维护状态的必要。

jQuery 核心方法应该被看作是中间层抽象。对开发者来说,更直接的麻烦是 DOM API,外观是 jQuery 库如此容易使用的原因。

以我们所学的为基础,外观模式既简化了类的接口,还将类从它的使用者的代码解耦出来。这就使得我们能够以一种间接的方式与子系统交互,这种方式比直接的方式更不容易出错。外观模式的好处既有容易使用,并且在实现模式时空间占用小。

让我们来看看实际的模式应用。这是一个没有优化的代码示例,但是我们是利用外观模式来简化一个跨浏览器监听事件的接口。我们的实现是通过创建一个公共方法,它能在别的代码中使用,被用于执行检测特性是否存在的任务,这样它就可以提供一个安全的、跨浏览器的兼容方案。

  1. var addMyEvent = function(el, ev, fn) {
  2. if (el.addEventListener) {
  3. el.addEventListener(ev, fn, false);
  4. } else if (el.attachEvent) {
  5. el.attachEvent('on' + ev, fn);
  6. } else {
  7. el['on' + ev] = fn;
  8. }
  9. };

我们都很熟悉 jQuery 的 $(document).ready(..) 就是用的类似的方式。在它的内部实际是通过一个名为 bindReady() 的方法实现的,它的作用是:

  1. bindReady: function() {
  2. ...
  3. if ( document.addEventListener ) {
  4. // 使用方便的事件回调
  5. document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
  6. // window.onload 的回调,它总是会执行
  7. window.addEventListener( "load", jQuery.ready, false );
  8. // 如果使用的是 IE 的事件模式
  9. } else if ( document.attachEvent ) {
  10. document.attachEvent( "onreadystatechange", DOMContentLoaded );
  11. // window.onload 的回调,它将总是会执行
  12. window.attachEvent( "onload", jQuery.ready );
  13. ...

这是另一个外观模式的示例,在这个示例中,其它部分只能仅仅使用通过 $(document).ready(..) 暴露出来的有限接口,它更为复杂的实现过程被隐藏在了它的内部。

然而,外观模式没要限制要被单独来使用。它还可以同其他模式混合使用,例如模块模式。正如接下来所要看到的,我们的模块模式的实例包含了一些私有定义的方法。外观模式让我们能够通过更简单的 API 来访问这些方法。

  1. var module = (function() {
  2. var _private = {
  3. i: 5,
  4. get: function() {
  5. console.log('current value:' + this.i);
  6. },
  7. set: function(val) {
  8. this.i = val;
  9. },
  10. run: function() {
  11. console.log('running');
  12. },
  13. jump: function() {
  14. console.log('jumping');
  15. }
  16. };
  17. return {
  18. facade: function(args) {
  19. _private.set(args.val);
  20. _private.get();
  21. if (args.run) {
  22. _private.run();
  23. }
  24. }
  25. };
  26. })();
  27. // 输出:"current value: 10" 和 "running"
  28. module.facade({ run: true, val: 10 });

在这个例子中,调用 module.facade() 实际会触发模块内部一些列的私有行为,但是,这些都是用户不关心的。我们让使用者更容易的使用这些特性,而不必担心实现层的细节。

抽象中需要注意的点

外观模式通常没什么缺点,但有一个值得关注的问题是性能。也就是,我们必须确定抽象到外观是否有隐含的成本,如果有的话,是否使合理。回到 jQuery 库,我们大多数人都知道 getElementById("identifier")$("#identifier") 都能用于查找页面中是否有对应 ID 的元素。

但是,你知道 getElementById() 的性能要快很多个量级吗?来看看这个 jsPerf 测试,看一下在每个浏览器中测试的结果:https://jsperf.com/query-by-id。当然,我们要记住 jQuery(和 Sizzle - 它的选择器引擎)在背后做了很多来优化我们的查询(并且返回的是一个 jQuery 对象,而不是一个 DOM 节点)。

上述这个特定外观的难点是要提供一个优雅的选择器函数来接收和解析多种类型的查询,这个抽象具有隐形的成本。用户不需要访问 jQuery.getById("identifier") 或者 jQuery.getByClass("identifier") 之类的方法。也就是说,性能的权衡已经经过了多年实践的测试,鉴于 jQuery 的成功,一个简单的外观对团队来说实际效果非常好。

当你使用这个模式的时候,试着关注下任何性能的消耗,并思考它们是否值得抽象到这个层面程度。