设计模式中的一些原则

单一职责原则

定义

对于一个类而言,应该只有一个引发它变化的缘由。在JavaScript中,单一职责原则更多地是被运用在对象或者方法级别上。单一职责原则(SRP)的职责定义为“引发变化的缘由”,若是咱们有多个动机去改写一个方法,那这个方法就对应多个职责。若是一个方法承担了过多职责,在需求的变迁过程当中,须要改写这个方法的可能性就越大。所以咱们能够总结SRP原则:一个对象或者方法只作一件事。javascript

设计模式中的SRP原则

SRP原则在不少模式中有着普遍的应用,例如代理模式、装饰者模式等。

代理模式
在我写的代理模式一文中,有一个图片预加载的例子,经过增长虚拟代理的方式,把图片预加载的职责放到代理对象中,而本体仅仅负责往页面添加img标签。
myImage负责往页面添加img标签:html

var myImage = (function () {
  var imgNode = document.createElement('img')

  document.body.appendChild(imgNode)

  return {
    setSrc (src) {
      imgNode.src = src
    }
  }
})()
复制代码

proxyImage负责预加载图片,并加载完图片后把请求交给本体myImage:java

var proxyImage = (function () {
  var img = new Image()

  img.onload = function () {
    myImage.setSrc(this.src)
  }

  return {
    setSrC (src) {
      myImage.setSrc('loading.gif') 
      img.src= src
    }
  }
})()

proxyImage.setSrc('http://xxx.com/01.jpg')
复制代码

这样把向页面添加img标签的功能和预加载图片的职责分开放到两个对象中,每一个对象只有一个被修改的意图,并且修改其中一个对象也不会影响另外一个对象。

装饰者模式
使用装饰者模式的时候,咱们一般让类或者对象只有一些基础的职责,更多的职责在代码运行时被动态地装饰到对象上,这也是分离职责的一种方式。
装饰者模式这篇文章中,咱们把数据上报的功能单独放在一个函数里,而后把这个函数动态地装饰到业务函数上:git

Function.prototype.after = function (fn) {
  var self = this
  return function () {
    var ret = _self.apply(this, arguments)
    fn.apply(this, arguments)

    return ret
  }
} 

var showLogin = function () {
  console.log('打开登陆弹窗')
}

var log = function () {
  console.log('上报数据')
}

document.getElementById('loginBtn').onclick = showLogin.after(log)
复制代码

什么时候分离职责

首先明确一点,并非全部的职责都应该一一分离。
若是随着需求的变化,有两个职责老是同时变化,那就不分离他们。好比在ajax请求的时候,建立xhr对象和发送xhr请求几乎都是一块儿的,那么这两个职责就没有必要分离。
职责的变化轴线仅当它们肯定会发生变化时才具备意义,即便两个职责已经被耦合在一块儿,但它们没有发生改变的预兆,也没有必要主动分离它们,等代码重构时分离也不迟。程序员

违反SRP原则

在人的常规思惟中,老是习惯性地把一组相关行为放到一块儿,如何正确地分离职责不是容易的一件事情。

一方面,咱们接受SRP原则的指导,另外一方面,咱们也没有必要任什么时候候都一成不变地遵照规则。 在实际开发中,由于种种缘由违反SRP原则的状况并很多见。好比jQuery的attr等方法,即负责赋值,又负责取值,这明显违反了SRP原则。对于jQuery维护者来讲,会有必定困难,可是对用户来讲,却简化了api的使用。

在方便性与稳定性之间要有一些取舍,具体是选择方便性仍是稳定性,取决于具体的应用场景github

SRP原则的优缺点

SRP的优势是下降了单个类或者对象的复杂度,按照职责把对象分解成更小的粒度,这有助于代码复用和进行单元测试
它最明显的缺点就是会增长编写代码的难度。其次,当咱们按职责把对象分解成小的粒度以后,实际上也增长了对象之间互相联系的难度。web

最小知识原则

定义

最少知识原则(LKP)说的是一个软件实体应当尽可能减小与其它实体发生相互做用,在面向对象中,指的就是在程序设计的时候,应当尽可能减小对象之间的交互。若是两个对象没必要直接通讯,那么这两个对象就不要发生直接的相互联系。ajax

设计模式中的最小知识原则

最少知识原则在设计模式中体现最多的是中介者模式和外观模式,可是外观模式在JavaScript中不多用,因此这里就不介绍了。

中介者模式
中介者模式一文中,咱们经过一个泡泡糖游戏的例子来学习中介者模式。当游戏有成千上万的玩家对战的时候,若是经过玩家互相引用达到通知游戏状态的目的,那实现起来代码将没法维护。可是经过引入一个中介者的方式,解耦全部玩家之间的直接联系,当一个玩家的状态改变时,只须要经过中介者对象来通知便可。

封装在最少知识原则中的体现
封装在很大程度上表达的是数据隐藏,一个模块或者对象将内部的数据或者实现细节隐藏起来,只暴露必要的接口给外界访问。对象之间不免产生联系,当一个对象必须引用另外一个对象,经过只暴露必要的接口从而让对象之间的联系限制在最小的范围以内。设计模式

开放-封闭原则

定义

在面向对象程序中,开放-封闭原则是最重要的一条原则。不少时候,一个程序具备良好的设计,它一般是符合开放-封闭原则的。开放封闭原则指的就是:一个对象(类、函数、模块)等应该是能够扩展的,可是不可修改api

扩展onload函数

假如咱们在维护一个大型的web项目,这个项目已经有必定的历史,也有不少人维护,代码已经有十万行。这时候,你接到一个需求,须要在window.onload以后,上报必定的数据。这个对开发来讲固然没什么难度,因而打开页面代码加上一行:

window.onload = function () {
  log('上报数据')
}
复制代码

在需求变动的过程当中,咱们常常是找到相关代码,而后修改它,这彷佛是理所固然的。可是若是想象一下,目前的window.onload函数是一个有几百行代码的巨型函数,里面遍及着各类变量和业务逻辑,若是需求更复杂,就可能会改好一个bug,产生5个bug。因而,咱们经过在AOP来动态地给window.onload增长新功能:

Function.prototype.after = function (fn) {
  var self = this
  return function () {
    var ret = _self.apply(this, arguments)
    fn.apply(this, arguments)

    return ret
  }
} 

window.onload = (window.onload || function () {}).after(function () {
  // 添加咱们新的业务代码
})
复制代码

经过动态装饰函数的方式,咱们彻底不用理会从前window.onload函数的内部实现。

用对象的多态性消除分支语句

过多的条件分支是形成程序违反开放-封闭原则的一个常见缘由,每当须要增长一个新的if语句时,都被迫要改动原函数。把if换成switch是没有用的,这是一种换汤不换药的作法。实际上,当咱们看到大量的if或者switch语句时,就能够考虑使用对象的动态性来重构它们。

下面是一种反例的实现:

var makeSound = function (animal) {
  if (animal instanceof Duck) {
    console.log('嘎嘎嘎')
  } else if (animal instanceof Chicken) {
    console.log('咯咯咯')
  }
}

var Duck = function () {}
var Chicken = function () {}

makeSound(new Duck())      // 嘎嘎嘎
makeSound(new Chicken())   // 咯咯咯
复制代码

增长了一种狗的类型,必须修改代码:

var makeSound = function (animal) {
  if (animal instanceof Duck) {
    console.log('嘎嘎嘎')
  } else if (animal instanceof Chicken) {
    console.log('咯咯咯')
  } else if (animal instanceof Dog) {
    console.log('汪汪汪')
  }
}

const Dog = function () {}
makeSound(new Dog())   // 汪汪汪
复制代码

利用多态的实现,把程序中不变的部分隔离出来(动物会叫),而后把可变的部分封装起来(不一样的动物发出不一样的叫声),这样程序就有了扩展性:

var makeSound = function (animal) {
  animal.sound()
}

var Duck = function () {}
Duck.prototype.sound = function () {
  console.log('嘎嘎嘎')
}

var Chicken = function () {}
Chicken.prototype.sound = function () {
  console.log('咯咯咯')
}

var Dog = function () {}
Dog.prototype.sound = function () {
  console.log('汪汪汪')
}

makeSound(new Duck())      // 嘎嘎嘎
makeSound(new Chicken())   // 咯咯咯
makeSound(new Dog())       // 汪汪汪
复制代码

找出变化的地方

指导咱们实现开放-封闭原则的规律就是:找出程序中常常发生变化的地方,而后把变化封装起来。
经过封装变化,咱们能够把系统中稳定的部分和容易变化的部分隔离开来,在系统的演变过程当中,咱们只须要替换那些容易变化的部分,由于这部分已经封装好了,因此替换起来也相对容易。

除了利用对象的多态性以外,下面还有一些方式能够帮助咱们编写遵照开放-封闭原则的代码:

  • 放置挂钩(hook)
    放置挂钩也是一种分离变化的方式。咱们在程序有可能变化的地方放置一个hook,根据hook返回的结果来决定程序下一步走向。这样一来,本来代码的执行路径上就出现了分叉路口,程序将来的执行方向有了多种可能。关于hook的应用,能够参考模板方法模式中hook的应用。
  • 使用回调函数
    在JavaScript中,函数能够做为参数传递给另一个函数,这也是高阶函数的应用之一。在这种状况下,咱们一般把这个函数称为回调函数,在JavaScript中,命令模式策略模式均可以使用回调函数轻松实现。
    回调函数是一种特殊的挂钩,咱们能够把容易变化的逻辑封装在回调函数里,而后把回调函数看成参数传入一个稳定和封闭的函数中。当函数执行,程序就能够根据回调函数的内部逻辑不一样,产生不一样的结果。
    例如,在ajax异步请求用户信息以后要作一些事,请求用户信息的过程是不变的,可是获取到用户信息以后要作的操做,则是可能变化的:
var getUserInfo = function (callback) {
  $.ajax('http://xxx.com/getUserInfo', callback)
}

getUserInfo(function (data) {
  console.log('更新cookie')
})

getUserInfo(function (data) {
  console.log('更新我的主页信息')
})
复制代码

开放-封闭原则的相对性

职责链模式中,也许会有人疑问:开放-封闭原则要求咱们只能经过增长源码的方式来扩展程序的功能,而不容许修改源码。当咱们往职责链增长一个新的订单100节点时,也必需要改动链条的代码:

order500.setNextSuccessor(order200).setNextSuccessor(orderNormal)
// 修改:

order500.setNextSuccessor(order200).setNextSuccessor(order100).setNextSuccessor(orderNormal)
复制代码

实际上,让程序保持彻底封闭是很难作到的,就算能作到也须要花太多时间和精力。并且让程序符合开放-封闭原则的代价是引入更多抽象层次,这也会增长代码的复杂度。在有些状况下,咱们不管如何都是作不到彻底封闭的,这时候咱们就要明白下面两点:

  • 挑选出最容易发生变化的地方,而后构造抽象来封装这些变化。
  • 在不可避免发生修改的时候,尽可能修改那些相对容易修改的地方。拿开源库来讲,修改它提供的配置文件,总比修改它的源码简单。

接受第一次愚弄

引用Bob大叔的《敏捷软件开发原则、模式与实践》:

有句古老的谚语:“愚弄我一次,应该羞愧的是你。再次愚弄我,应当羞愧的是我。”这也是一种有效对待软件设计的态度。为了防止软件背着没必要要的复杂性,咱们容许本身被愚弄一次。

让程序一开始就尽可能遵照开放-封闭原则,并非一件容易的事情。首先,咱们须要知道程序哪些地方会发生变化,这要求咱们能提早预想到未来的一些需求变化。其次,留给开发程序员的需求开发周期是有限的,因此咱们能够说服本身接受不合理代码的第一次愚弄。在需求开发的时候,咱们能够先迅速完成需求,而后再回头找出变化的地方封装起来。

相关文章
相关标签/搜索