设计原则和风格指南

设计原则和风格指南

单一责任原则 (SRP)

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

此时,这个方法通常是一个不稳定的方法,修改代码总是一件危险的时,特别是两个职责耦合在一起的时候,一个职责发生变化可能会影响到其他职责的实现,造成意想不到的破坏,这种耦合性得到的是低内聚和脆弱的设计。

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

设计模式中的单一责任原则

举一个创建单例弹窗的例子

const createSingleDialog = (() => {
  let instance = null
  return () => {
		if (!instance) {
      instance = document.createElement('div')
      instance.innerHTML = '弹窗的内容...'
      instance.style.display = 'none'
      document.body.appendChild(instance)
    }
    return instance
  }
})()

const dialog = createSingleDialog()

根据上述示例,createSingleDialog方法即做了管理单例,又做了创建单例的两个事,显然违背了SRP原则。

我们把管理单例的职责和创建单例的职责分别封装在两个方法里,这两个方法可以独立变化而又互不影响,当它们连接在一起时,就完成了创建唯一弹窗的功能,下面代码显然是更好的做法:

// 获取单例
const getSingle = (creatorFn) => {
  let result = null
  return (...args) => {
    return result || (result = creatorFn(...args))
  }
}

// 创建弹窗
const createDialog = () => {
  const instance = document.createElement('div')
  instance.innerHTML = '弹窗的内容...'
  instance.style.display = 'none'
  document.body.appendChild(instance)
  return instance
}

const createSingleDialog = getSingle( createDialog )

const dialog1 = createSingleDialog()
const dialog2 = createSingleDialog()

console.log(dialog1 === dialog2) // 输出:true

此时如果我还想创建一个单例的iframe,也只需添加一个创建iframe的职责即可

const createIframe = () => {
  const iframe = document.createElement('iframe')
  document.body.appendChild(iframe)
  return iframe
}

const createSingleIframe = getSingle( createIframe )
const singleIframe = createSingleIframe()
singleIframe.src = 'https://www.sto.cn'

SRP原则的优缺点

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

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

何时应该分离职责

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

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

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

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

开发-封闭原则(OCP)

开放和封闭

开放-封闭原则最早由Eiffel语言的设计者Bertrand Meyer在其原著作Object-Oriented Software Construction中提出。它的定义如下:

软件实体(类,模块,函数)等应该是可以扩展的,但是不可修改

在明白开放-封闭原则的定义之前,先看一个示例:

假设我们是一个大型Web项目的维护人员,在接手这个项目时,发现它已经有10万行以上的js代码和数百个js文件

不久后接到了一个新的需求,即在window.onload函数中打印出页面中所有节点数量。这当然不难,于是我们打开编辑器,搜索出window.onload函数的位置,在函数内部添加如下代码:

window.onload = () => {
  // 省略原有代码
  console.log( document.getElementsByTagName('*').length )
}

如果目前的window.onload已经是一个拥有500行代码的函数,里面密布着何种变量和交叉的业务逻辑,而我们的需求又不仅仅是打印一个log这个简单。那么“改好一个bug,引发其他bug”这样的事情就真的可能发生。我们永远不知道刚刚的改动会有什么副作用,很可能会引发一些列的连锁反应。那么,有没有办法在不修改代码的情况下,就能满足新需求呢?通过添加代码,而不是修改代码的方式,来给window.onload添加新的功能,代码如下:

Function.prototype.after = function (afterFn) {
  const _self = this
  return function(...args){
    const ret = _self.apply(this, args)
    afterFn.apply(this, args)
    return ret
  }
}
window.onload = ( window.onload || function () {} ).after(function(){
  console.log( document.getElementsByTagName('*').length )
})

现在可以引出开放-封闭原则的思想:当需要改变一个程序的功能或者给这个程序添加新功能的时候,可以使用添加代码的方式,但是不允许改动程序的源代码。

开放-封闭原则的相对性

让程序一开始就尽量遵循开放-封闭原则,并不是一件容易的事。一方面,我们需要尽快知道程序在哪些地方会发生变化,这要求我们有一些“未卜先知”的能力。另一方面,留给程序员的需求排期并不是无限的,所以我们可以说服自己去接受不合理的代码带来的第一次折腾。在最初编写代码的时候,先假设变化永远不会发生,这有利于我们迅速完成需求。当变化发生时并且队我们接下来的工作造成影响的时候,可以再回过头来封装这些变化的地方。然后确保我们不会掉进同一个坑里面。

Loading...