四个你应该知道的Javascript设计模式【译】

  • 原文地址:https://scotch.io/bar-talk/4-javascript-design-patterns-you-should-know

前言

  • 每位程序开发者都在致力于编写可维护、可读性高以及可复用的代码。随着应用越来越大,代码的结构化也变得更加重要了。设计模式(design pattern)就是用来解决这个问题的关键,通过一些通用的代码组织结构来解决一些特定情况下的常见问题。
  • JavaScript的web开发者在开发应用时,经常与设计模式打交道,尽管可能你并没有意识到~
  • 虽然有一长串各种各样的设计模式(GOF四人帮提出了23种),但在JavaScript中可能通常只会用到其中几个。
  • 本文将讨论一些常见的设计模式,希望能给你提供一些提升编程能力的方法,以及更加地深入js底层原理。
  • 下面将介绍四种设计类型:
    • 模块模式(Module)
    • 原型模式(Prototype)
    • 观察者模式(Observer)
    • 单例模式(Singleton)
  • 每个设计模式由许多元素组成,然而我会着重于以下几个关键点:
    1 应用场景(Context):在哪里或者在什么情况下可以使用该模式
    2 问题(Problem):我们要用来解决什么问题
    3 解决方案(Solution):我们要如何应用设计模式来解决我们提出的问题
    4 实现(Implementation):具体实现起来是怎么样的

模块化设计模式(Module Design Pattern)

  • JS模块化也是一种设计模式,普遍用来使部分代码独立于其他组件代码。这样代码之间耦合性更弱,从而开发出更高质量的代码。
  • 对于熟悉面向对象语言(比如Java)的开发者来说,js中的模块化就相当于"Class类"。类的众多优点之一就是封装encapsulation--保护自己的变量和方法不被其他类访问。模块化设计模式允许公共和私有两种访问级别。
  • 模块代码应该是通过的立即执行函数(IIFE)来产生私有作用域,换句话说,使用一个闭包来产生私有变量和方法(然而最后还是会返回一个变量),代码大致像下面这样:
(function() {
    // 在这里声明私有变量或方法
    return {
      // 在这里声明公有变量和方法
    }
})();
  • 在返回一个我们想要返回的对象之前,先声明好私有变量和方法。在闭包外部的代码无法访问闭包内部的私有变量和方法,因为他们不在同一个作用域内。下面来看一个更加具体的应用:
var HTMLChanger = (function() {
  var contents = 'contents'
  var changeHTML = function() {
    var element = document.getElementById('attribute-to-change');
    element.innerHTML = contents;
  }
  return {
    callChangeHTML: function() {
      changeHTML();
      console.log(contents);
    }
  };
})();
HTMLChanger.callChangeHTML();       // 输出: 'contents'
console.log(HTMLChanger.contents);  // undefined
  • 注意callChangeHTML方法被绑定到闭包返回的对象上了,在HTMLChanger的作用域下可以被引用,但是在闭包外部,content属性就不能被访问了。
揭示型模块模式
  • 揭示型模块模式(Revealing Module Pattern)是模块模式的一种变形,目的在于保持模块封装的前提下,通过返回的对象暴露几个特点的属性或方法。一个直观的应用像下面这样:
var Exposer = (function() {
  var privateVariable = 10;
  var privateMethod = function() {
    console.log('Inside a private method!');
    privateVariable++;
  }
  var methodToExpose = function() {
    console.log('This is a method I want to expose!');
  }
  var otherMethodIWantToExpose = function() {
    privateMethod();
  }
  return {
      first: methodToExpose,
      second: otherMethodIWantToExpose
  };
})();

Exposer.first();        // Output: This is a method I want to expose!
Exposer.second();       // Output: Inside a private method!
Exposer.methodToExpose; // undefined
  • 虽然代码看起来更加整洁,一个明显的缺点就是无法直接引用私有方法。这样在进行单元测试时会遇到些挑战,同样的,公有行为也是不可重写的。

原型设计模式(Prototype Design Pattern)

  • 可能有的js开发者既搞不清楚原型继承和关键字prototype,也不会在代码中应用到原型。原型设计模式依赖于JavaScript的原型链继承。大多数情况下,原型主要用来创建对象。
  • 对象创建的过程是对传递过去的原对象进行浅克隆。原型设计模式的其中一个使用实例就是:执行一个大量的数据操作来创建一个供应用其他部分使用的对象。如果另一个程序要使用这个对象,和执行大量的数据操作相比,直接克隆之前创建好的对象就有利的多了。


    四个你应该知道的Javascript设计模式【译】_第1张图片
    image
  • 上图介绍了一个原型接口如何用来克隆的具体应用
  • 克隆一个对象时,必须要有一个构造函数来实例化第一个对象。然后,再通过使用prototype关键字来把变量和方法绑定到对象结构上去。来看下面这个基础的例子:
var TeslaModelS = function() {
  this.numWheels    = 4;
  this.manufacturer = 'Tesla';
  this.make         = 'Model S';
}

TeslaModelS.prototype.go = function() {
  // Rotate wheels
}

TeslaModelS.prototype.stop = function() {
  // Apply brake pads
}
  • 构造函数用来产生一个单独的TeslaModelS对象,在创建一个新的TeslaModelS对象时,将会保持构造函数里声明的变量。另外,我们通过关键字prototype声明时,方法gostop也会保留给对象实例。使用下面的代码也是同一个意思:
var TeslaModelS = function() {
  this.numWheels    = 4;
  this.manufacturer = 'Tesla';
  this.make         = 'Model S';
}

TeslaModelS.prototype = {
  go: function() {
    // Rotate wheels
  },
  stop: function() {
    // Apply brake pads
  }
}

揭示型原型模式

  • 和模块模式类似,原型模式也有揭示型的变种。揭示型原型模式提供一个对私有成员和公有成员的封装,同时返回一个字面量对象
  • 因为我们是要返回一个对象,所以我们要用一个函数来表达原型对象。通过对上述例子的扩展,在当前原型内部,可以选择想要暴露的属性或方法:
var TeslaModelS = function() {
  this.numWheels    = 4;
  this.manufacturer = 'Tesla';
  this.make         = 'Model S';
}

TeslaModelS.prototype = function() {

  var go = function() {
    // Rotate wheels
  };

  var stop = function() {
    // Apply brake pads
  };

  return {
    pressBrakePedal: stop,
    pressGasPedal: go
  }

}();
  • 注意gostop方法是被保护了的,因为他们不在所返回的对象所在的作用域内。因为js本来就支持原型继承,所以也不需要再重写底部特性了。

观察者模式

  • 很多时候当应用中的一部分代码变化时,要求其他部分也进行更新。在AngularJS中,当$scope对象更新时,就会触发一个事件去通知其他组件。观察者模式包含的内容就是:如果一个对象发生改变了,那么它就会广播(broadcasts)通知所有的依赖于此的对象。
  • 另一个最简单的例子就是MVC结构(比如backbone框架);model改变时,view视图层就会更新。有个好处就在于解耦view和model,使代码依赖性降低。


    四个你应该知道的Javascript设计模式【译】_第2张图片
    image
  • 从上面的图表可以看出,必须要的对象是subject, observer, 和 concrete对象。subject包含了各个observer观察者实例的索引,从而能通知任何发生的改变。Observer是一个抽象类,允许观察者实例执行通知方法。
  • 来看下用AngularJS通过事件管理来实现观察者模式的例子。
// Controller 1
$scope.$on('nameChanged', function(event, args) {
    $scope.name = args.name;
});
...
// Controller 2
$scope.userNameChanged = function(name) {
    $scope.$emit('nameChanged', {name: name});
};
  • 使用观察者模式时,区分subject和依赖对象是很重要的。
  • 虽然观察者模式能提供很多好处,但还是有缺陷,比如随着观察者对象数量的增加,程序的性能会明显下降。一个臭名昭著的观察者就是watchers。在AngularJS中,我们可以watch监控变量、函数和对象。当作用域中的一个对象改变时,$$digest循环执行同时通知每一个相关观察者。
  • 我们可以在JavaScript代码中创建观察者(Observers)和目标对象(Subjects)。我们来看下是如何实现的:
var Subject = function() {
  this.observers = [];

  return {
    subscribeObserver: function(observer) {
      this.observers.push(observer);
    },
    unsubscribeObserver: function(observer) {
      var index = this.observers.indexOf(observer);
      if(index > -1) {
        this.observers.splice(index, 1);
      }
    },
    notifyObserver: function(observer) {
      var index = this.observers.indexOf(observer);
      if(index > -1) {
        this.observers[index].notify(index);
      }
    },
    notifyAllObservers: function() {
      for(var i = 0; i < this.observers.length; i++){
        this.observers[i].notify(i);
      };
    }
  };
};

var Observer = function() {
  return {
    notify: function(index) {
      console.log("Observer " + index + " is notified!");
    }
  }
}

var subject = new Subject();

var observer1 = new Observer();
var observer2 = new Observer();
var observer3 = new Observer();
var observer4 = new Observer();

subject.subscribeObserver(observer1);
subject.subscribeObserver(observer2);
subject.subscribeObserver(observer3);
subject.subscribeObserver(observer4);

subject.notifyObserver(observer2); // Observer 2 is notified!

subject.notifyAllObservers();
// Observer 1 is notified!
// Observer 2 is notified!
// Observer 3 is notified!
// Observer 4 is notified!

发布/订阅(Publish/Subscribe)

  • 发布/订阅模式:在希望接收通知的对象(订阅者)和触发事件的对象(发布者)之间,搭建一个主题/事件的通道。这个事件系统运行代码定义具体的应用事件,来传递订阅者需要的参数值。这里的思想是在于,避免发布者和订阅者相依赖(即他们是通过事件来联系的,并没有直接的依赖)。
  • 这和观察者模式的区别在于:订阅者执行一个适当的事件处理函数来进行注册,然后接收发布者的广播通知。
  • 许多开发者将发布/订阅模式与观察者相结合,尽管两者可能有些区别。在发布/订阅模式中的订阅者,通过一些媒介信息得到通知,然而观察者是通过执行一个处理函数来得到通知。

单例模式(Singleton)

  • 一个单例(Singleton)类,只允许存在唯一一个实例。单例模式会禁止客户端创建多个实例对象,当第一个实例被创建后,后面就直接返回这一个实例本身,而不会去创建另一个实例。
  • 在你没使用过单例模式时,是很难去发现单例的用例。举个例子,比如办公室的打印机,办公室里有十个人,他们分别有自己的电脑,然后共用一台打印机。这台打印机就相当于单例类的唯一实例,然后共享打印机的同时,共享着相同的资源。
var printer = (function () {
  var printerInstance;
  function create () {
    function print() {
      // underlying printer mechanics
    }
    function turnOn() {
      // warm up
      // check for paper
    }
    return {
      // public + private states and behaviors
      print: print,
      turnOn: turnOn
    };
  }
  return {
    getInstance: function() {
      if(!printerInstance) {
        printerInstance = create();
      }
      return printerInstance;
    }
  };
  function Singleton () {
    if(!printerInstance) {
      printerInstance = intialize();
    }
  };
})();
  • create方法应该是私有的,因为我们不想客户端能够访问它。但是getInstance方法是公共的,每个办公室的职员想要使用打印机时,就会去调用getInstance方法来得到唯一实例,第一次创建成功后,后面再次调用就直接返回之前已经创建好的实例对象:
var officePrinter = printer.getInstance();
  • 在AngularJS中,单例模式是很常见的,比如作为services, factories, 和 providers。因为他们在保持状态、提供可访问资源,如果创建两个实例就违背唯一共享services/factories/providers的设计理念了。
  • 在多线程的应用中,当多个线程同时访问一个资源时就会产生竞态条件(race condition)。单例模式就很容易受到竞态条件的影响,比如一开始并没有创建实例对象,两个进程同时访问,然后同时创建了一个实例,就会产生了两个实例对象,而并不是先创建一个,然后再返回之前创建好的。这就违背了单例模式的本意了。所以开发者在多线程应用中使用单例时,要了解同步性。(然而js只是单线程的~~~)

结论

  • 设计模式在大型应用中经常会用到,理解其中一两种也是很有好处的,伴随着实践效果更佳。
  • 在构造一个应用之前,你应该彻底思考清楚,一个模块应该如何和其他模块相联系。回顾熟悉上面四个设计模式之后,你应该能够在项目中识别出他们,并广泛的应用。

你可能感兴趣的:(四个你应该知道的Javascript设计模式【译】)