第十八章 单一职责原则

就一个类而言,应该仅有一个引起它变化的原因。在 JavaScript 中,用到类的场景并不算多,我们通常将单一职责原则用于对象或者方法上。

单一职责原则(SRP)的职责被定义为“引起变化的原因”。如果我们有两个动机去改写一个方法,那么这个方法就具有两个职责。每个职责都是一个变化的轴线,如果一个方法承担了太多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。

此时,这个方法是一个不稳定的方法,修改代码总是一件危险的事情,特别是当两个职责耦合在一起的时候,一个职责发生变化可能会影响到其他职责的实现。这种耦合性是低内聚的设计。

SRP 原则体现为:一个对象(方法)只做一件事。

18.1 设计模式的 SRP 原则

  1. 代理模式

    图片预加载就是采用单一职责模式的,我们的本体函数的职责是给在页面添加 img 标签,而代理对象的函数的职责是预加载图片,并在预加载完成之后把请求交给本体。

    js
    1const img = (function () {
    2  const img = document.createElement('img')
    3  document.body.appendChild(img)
    4  return {
    5    setSrc(src) {
    6      img.src = src
    7    },
    8  }
    9})()
    10
    11const proxyImg = (function () {
    12  const proxyImg = document.createElement('img')
    13  proxyImg.onload = function () {
    14    img.setSrc(this.src)
    15  }
    16  return {
    17    setSrc(src) {
    18      img.setSrc('../九尾.jpg')
    19      setTimeout(() => {
    20        proxyImg.src = src
    21      }, 1000)
    22    },
    23  }
    24})()
    25
    26proxyImg.setSrc('../umi-demo-1.gif')

    把添加 img 的功能和预加载图片的功能分开放到两个对象中,这两个对象各自都只有一个被修改的动机。在它们各自发生改变时,也不会影响到另外的对象。

  2. 单例模式

    我们把负责管理单例的职责和负责创建登录浮窗的职责分别封装在两个方法里,这两个方法可以独立变化而不受影响。当它们连接在一起时,就完成了创建唯一登录框的功能。

    js
    1var getSingle = function (fn) {
    2  let div = null
    3  getSingle = function (fn) {
    4    if (!div) {
    5      div = fn.apply(this, arguments)
    6    }
    7    return div
    8  }
    9  getSingle(fn)
    10  return getSingle
    11}
    12getSingle(function () {})
    13
    14var createLoginLayer = function () {
    15  var div = document.createElement('div')
    16  div.innerHTML = '我是登录浮窗'
    17  document.body.appendChild(div)
    18  return div
    19}
    20
    21const button1 = getSingle(createLoginLayer)
    22const button2 = getSingle(createLoginLayer)
    23console.log(button1 === button2) // true
  3. 装饰者模式

    使用装饰者模式的时候,我们通常让类或者对象一开始只具有一些基础的职责,更多的职责在代码运行时被动态地装饰到对象上面。装饰者模式可以为对象动态增加职责,这也是分离职责的一种方式。

    我们在动态模式中,使用 AOP 的方式来给函数动态地添加职责。

    js
    1Function.prototype.after = function (fn) {
    2  const self = this
    3  return function (...rest) {
    4    const result = self.call(this, ...rest)
    5    fn.call(this, ...rest)
    6    return result
    7  }
    8}
    9function log1() {
    10  console.log(1)
    11}
    12function log2() {
    13  console.log(2)
    14}
    15const log = log1.after(log2)
    16log()

18.2 何时应该分离职责

SRP 原则是所有原则中最简单的也是最难运用的原则之一。

要明确的事,并不是所有职责都应该一一分离。

一方面,随着需求的变化,两个职责总是同时变化,那就不必分离它们。比如创建 xhr 对象和发送 xhr 请求几乎总是在一起,那就没必要分离。

另一方面,职责的变化轴线仅当它们确定发生变化时才具有意义,即使两个职责以及耦合在一起,但它们还没有发生改变的征兆,那么没有必要主动分离它们,在代码重构时再分离也不迟。

18.3 违反 SRP 原则

在人的常规思维中,总是习惯把一组相关的行为放在一起,如何正确分离职责不是一件容易的事情。

对于 SRP 原则,许多专家委婉表示“this is sometimes hard to see”

我们虽然受设计原则的指导,但是我们未必要在任何时候都一成不变地遵守原则。在实际开发中,因为某些原因违反 SRP 原则的情况并不少见。

jQuery 的 attr 是个非常庞大的方法,既负责赋值,又负责取值,对于 jQuery 的维护者来说可能会带来一些困难,但是对于使用者来说,简化了用户的使用。

在方便性与稳定性之间总是要有一些取舍,这取决于具体的应用环境。

18.4 SRP 原则的优缺点

SRP 原则的优点时降低了单个类或者对象的复杂度,按照职责把对象分成更小的粒度,这有助于代码的复用。当一个职责需要变更时,不会影响到其他职责。

SRP 的缺点则是明显增加编写代码的复杂度,当我们按照职责把对象分解成更小的粒度后,实际上也是增加了这些对象之间相互联系的难度。