讀懂 SOLID 的「單一職責」原則

這是理解 SOLID原則中,關於 單一職責原則如何幫助咱們編寫低耦合和高內聚的第二篇文章。

單一職責原則是什麼

以前的第一篇文章闡述了依賴倒置原則(DIP)可以使咱們編寫的代碼變得低耦合,同時具備很好的可測試性,接下來咱們來簡單瞭解下單一職責原則的基本概念:前端

Every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.

每個模塊或者類所對應的職責,應對應系統若干功能中的某個單一部分,同時關於該職責的封裝都應當經過這個類來完成。react

往簡單來說:程序員

A class or module should have one, and only one, reason to be changed.

一個類或者模塊應當用於單一的,而且惟一的原因被更改。編程

若是僅僅經過這兩句話去理解, 一個類或者模塊若是若是越簡單(具備單一職責),那麼這個類或者模塊就越容易被更改是有一些困難的。爲了便於咱們理解整個概念,咱們將分別從三個不一樣的角度來分析這句話,這三個角度是:redux

  • Single: 單一
  • Responsibility: 職責
  • Change: 改變

什麼是單一

Only one; not one of several.

惟一的,而不是多箇中的某個。segmentfault

Synonyms: one, one only, sole, lone, solitary, isolated, by itself.設計模式

同義詞:一,僅有的一個,惟一,獨個,獨自存在的,孤立的,僅本身。api

單一意味着某些工做是獨立的。好比,在類中,類方法僅完成某家獨立的事情,而不是兩件,以下:架構

class UserComponent { 
  // 這是第一件事情,獲取用戶詳情數據
  getUserInfo(id) {
    this.api.getUserInfo(id).then(saveToState)
  }

  // 這是第二件事情,渲染視圖的邏輯
  render() {
    const { userInfo } = this.state;
    return <div>
      <ul>
        <li>Name: { userInfo.name }</li>
        <li>Surname: { userInfo.surname }</li>
        <li>Email: { userInfo.email }</li>
      </ul>
    </div>
  }
}

看了上面的代碼,你可能很快就會聯想到,這些代碼基本存在於全部的React組件中。ide

確實,對於一些小型的項目或者演示型項目,這樣編寫代碼不會產生太大的問題。可是若是在大型或者複雜度很高的項目中,仍然按照這樣的風格,則是一件比較糟糕的事情,由於一個組件每每作了它本不該當作的事情(承擔了過多的職責)。

這樣會帶來什麼壞處呢?好比對於以上的api服務,在未來的某天你作出了一些修改,增長了一些額外的邏輯,那麼爲了使代碼可以正常工做,你至少須要修改項目中的兩個地方以適應這個修改,一處修改是在API服務中,而另外一處則在你的組件中。若是進一步思考的,咱們會發現,修改次數與在項目直接使用API服務的次數成正比,若是項目足夠複雜,足夠大,一處簡單的邏輯修改,就須要作出一次貫穿整個系統的適配工做。

那麼咱們若是避免這種狀況的發生呢?很簡單,咱們僅僅須要將關於用戶詳情數據的邏輯提高到調用層,在上面的例子中,咱們應當使用React.component.prop來接受用戶詳情數據。這樣,UserComponent組件的工做再也不與如何獲取用戶詳情數據的邏輯耦合,從而變得單一

對於鑑別什麼是單一,什麼不是單一,有不少不一樣的方式。通常來講,只須要牢記,讓你的代碼儘量的少的去了解它已經作的工做。(譯者注:我理解意思應當是,應當儘量的讓已有的類或者方法變得簡單、輕量,不須要全部事情都親自爲之)

總之,不要讓你的對象成爲上帝對象

A God Object aka an Object that knows everything and does everything.

上帝對象,一個知道一切事情,完成一切事情的對象。

In object-oriented programming, a God object is an object that knows too much or does too much. The God object is an example of an anti-pattern.

在面向對象編程中,上帝對象指一個瞭解太情或者作太多事情的對象。上帝對象是反模式的一個典型。

什麼是職責

職責指軟件系統中,每個指派給特定方法、類、包和模塊所完成的工做或者動做。

Too much responsibility leads to coupling.

太多的職責致使耦合。

耦合性表明一個系統中某個部分對系統中另外一個部分的瞭解程度。舉個例子,若是一段客戶端代碼在調用class A的過程當中,必需要先了解有關class B的細節,那麼咱們說AB耦合在了一塊兒。一般來講,這是一件糟糕的事情。由於它會使針對系統自己的變動複雜化,同時會在長期愈來愈糟。

爲了使一個系統到達適當的耦合度,咱們須要在如下三個方面作出調整

  • 組件的內聚性
  • 如何測量每一個組件的預期任務
  • 組件如何專一於任務自己

低內聚性的組件在完成任務時,和它們自己的職責關聯並不緊密。好比,咱們如今有一個User類,這個類中咱們保存了一些基本信息:

class User {
  public age;  
  public name;
  public slug;
  public email;
}

對於屬性自己,若是對於每一個屬性聲明一些getter或者setter方法是沒什麼問題的。可是若是咱們加一些別的方法,好比:

class User {
  public age;  
  public name;
  public slug;
  public email;
  // 咱們爲何要有如下這些方法?
  checkAge();
  validateEmail();
  slugifyName();
}

對於checkAgevalidateEmailslugifyName的職責,與Userclass自己關係並不緊密,所以就會這些方法就會使User的內聚性變低。

仔細思考的話,這些方法的職責和校驗和格式化用戶信息的關係更緊密,所以,它們應當從User中被抽離出來,放入到另外一個獨立的UserFieldValidation類中,好比:

class User {
  public age;  
  public name;
  public slug;
  public email;
}

class UserFieldValidation {
  checkAge();
  validateEmail();
  slugifyName();
}

什麼是變動

變動指對於已存在代碼的修改或者改變。

那麼問題來了,什麼緣由迫使咱們須要對源碼進行變動?從衆多過時的軟件系統的歷史數據的研究來看,大致有三方面緣由促使咱們須要做出變動:

  • 增長新功能
  • 修復缺陷或者bug
  • 重構代碼以適配未來做出的變動

作爲一個程序員,咱們每天不都在作這三件事情嗎?讓咱們來用一個例子完整的看一下什麼是變動,比方說咱們完成了一個組件,如今這個組件性能很是好,並且可讀性也很是好,也許是你整個職業生涯中寫的最好的一個組件了,因此咱們給它一個炫酷的名字叫做SuperDuper(譯者注:這個名字的意思是超級大騙子

class SuperDuper {
  makeThingsFastAndEasy() {
    // Super readable and efficient code
  }
}

以後過了一段時間,在某一天,你的經理要求你增長一個新功能,好比說去調用別的class中的每一個函數,從而可使當前這個組件完成更多的工做。你決定將這個類以參數的形式傳入構造方法,並在你的方法調用它。

這個需求很簡單,只須要增長一行調用的代碼便可,而後你作了如下變動(增長新功能)

class SuperDuper {
  constructor(notDuper: NotSoDuper) {
    this.notDuper = notDuper
  }
  makeThingsFastAndEasy() {
     // Super readable and efficient code
    this.notDuper.invokeSomeMethod()
  }
}

好了,以後你針對你作的變動代碼運行了單元測試,而後你忽然發現這條簡單的代碼使100多條的測試用例失敗了。具體緣由是由於在調用notDuper方法以前,你須要針對一些額外的業務邏輯增長條件判斷來決定是否調用它。

因而你針對這個問題又進行了一次變動(修復缺陷或者bug),或許還會針對一些別的邊界條件進行一些額外的修復和改動:

class SuperDuper {
  constructor(notDuper: NotSoDuper) {
    this.notDuper = notDuper
  }
  makeThingsFastAndEasy() {
     // Super readable and efficient code
    
    if (someCondition) {
      this.notDuper.invokeSomeMethod()
    } else {
      this.callInternalMethod()
    }
  }
}

又過了一段時間,由於這個SuperDuper畢竟是你職業生涯完成的最棒的類,可是當前調用noDuper的方法實在是有點不夠逼格,因而你決定引入事件驅動的理念來達到不在SuperDuper內部直接調用noDuper方法的目的。

此次實際是對已經代碼的一次重構工做,你引入了事件驅動模型,並對已有的代碼作出了變動(重構代碼以適配未來做出的變動):

class SuperDuper {
 
  makeThingsFastAndEasy() {
     // Super readable and efficient code
     ...
     dispatcher.send(actionForTheNotDuper(payload)) // Send a signal
  }
}

如今再來看咱們的SuperDuper類,已經和最原始的樣子徹底不同了,由於你必須針對新的需求、存在的缺陷和bug或者適配新的軟件架構而作出變動。

所以爲了便於咱們作出變動,在代碼的組織方式上,咱們須要用心,這樣纔會使咱們在作出變動時更加容易。

如何才能使代碼貼近這些原則

很簡單,只須要牢記,使代碼保持足夠簡單。

Gather together the things that change for the same reasons. Separate those things that change for different reasons.

將因爲相同緣由而作出改變的東西彙集在一塊兒,將因爲不一樣緣由而作出改變的東西彼此分離。

孤立變化

對於所編寫的作出變動的代碼,你須要仔細的檢查它們,不管是從總體檢查,仍是有邏輯的分而治之,均可以達到孤立變化的目的。你須要更多的瞭解你所編寫的代碼,好比,爲何這樣寫,代碼到底作了什麼等等,而且,對於一些特別長的方法和類要格外關注。

Big is bad, small is good…

大便是壞,小便是好。

追蹤依賴

對於一個類,檢查它的構造方法是否包含了太多的參數,由於每個參數都做爲這個類的依賴存在,同時這些參數也擁有自身的依賴。若是可能的話,使用DI機制來動態的注入它們。

Use Dependency Injection

使用依賴注入

追蹤方法參數

對於一個方法,檢查它是否包含了太多參數,通常來說,一個方法的參數個數每每表明了其內部所實現的職能。

同時,在方法命名上也投入一精力,儘量地使方法名保持簡單,它將幫助你在重構代碼時,更好的達到單一職責。長的函數名稱每每意味着其內部有糟糕的味道。

Name things descriptively

描述性命名。

儘早重構

儘量早的重構代碼,當你看到一些代碼能夠以更簡明的方式進行時,重構它。這將幫助你在項目進行的整個週期不斷的整理代碼以便於更好的重構。

Refactor to Design Patterns

按設計模式重構代碼

善於作出改變

最後,在須要作出改變時,果斷地去作。固然這些改變會使系統的耦合性更低,內聚性更高,而不是往相反的方向,這樣你的代碼會一直創建在這些原則之上。

Introduce change where it matters. Keep things simple but not simpler.

在重要的地方介紹改變。保持事情的簡單性,但不是一味追求簡單。

譯者注

單一職責原則其實在咱們平常工做中常常會接觸到,比方說

  • 咱們常常會聽到DIY(dont repeat yourself)原則,其自己就是單一職責的一個縮影,爲了達到DIY,對於代碼中的一些通用方法,咱們常常會抽離到獨立的utils目錄甚至編寫爲獨立的工具函數庫, 好比lodashramda等等
  • OAOO, 指Once And Only Once, 原則自己的含義能夠自行搜索,實際工做中咱們對於相同只能模塊的代碼應當儘量去在抽象層合併它們,提供抽象類,以後經過繼承的方式來知足不一樣的需求
  • 咱們都會很熟悉單例模式這個模式,但在使用時必定要當心,由於本質上單例模式與單一職責原則相悖,在實踐中必定要具體狀況具體分析。同時也不要過分優化,就如同文章中最後一部分說起的,咱們要保證一件事情的簡單性,但不是一味地爲了簡單而簡單。
  • 前端的技術棧中,redux對於數據流層的架構思想,便充分體現了單一職責原則的重要性,action做爲對具體行爲的抽象, store用來描述應用的狀態,reducer做爲針對不一樣行爲如何對store做出修改的抽象。
  • react中常常說起的木偶組件(dump component)其實和文章中第一部分的例子一模一樣
  • 工廠模式命令模式也必定程度體現了單一職責原則,前者對於做爲生產者存在並不須要關心消費者如何消費對象實例,後者以命令的方式封裝功能自己就是單一職責原則的體現。

我可以想到的就這麼多,寫的比較亂,拋磚引玉,若有錯誤,還望指正。

關注公衆號 全棧101,只談技術,不談人生

clipboard.png

相關文章
相關標籤/搜索