設計模式中的一些原則

單一職責原則

定義

對於一個類而言,應該只有一個引發它變化的緣由。在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大叔的《敏捷軟件開發原則、模式與實踐》:

有句古老的諺語:「愚弄我一次,應該羞愧的是你。再次愚弄我,應當羞愧的是我。」這也是一種有效對待軟件設計的態度。爲了防止軟件揹着沒必要要的複雜性,咱們容許本身被愚弄一次。

讓程序一開始就儘可能遵照開放-封閉原則,並非一件容易的事情。首先,咱們須要知道程序哪些地方會發生變化,這要求咱們能提早預想到未來的一些需求變化。其次,留給開發程序員的需求開發週期是有限的,因此咱們能夠說服本身接受不合理代碼的第一次愚弄。在需求開發的時候,咱們能夠先迅速完成需求,而後再回頭找出變化的地方封裝起來。

相關文章
相關標籤/搜索