【譯】什麼是SOLID原則(第1部分)

gavinliu6發表於2019-03-03

翻譯自:What’s the deal with the SOLID principles?(part 1)

即使你是一個初級開發人員,你也可能聽說過 SOLID 原則。它們無處不在。工作面試時,你也許聽過這樣的問題:“你是如何評估程式碼質量的呢?又是如何區分程式碼的好壞呢?”一般的答案類似這樣:“我儘量保持檔案足夠小。當檔案變得很大時,我將移動部分程式碼到其他檔案中。”。最糟糕的答案之一是:“我看到了就知道了。”當你用此類的描述回答之後,面試官通常會問:“你聽說過 SOLID 原則嗎?”。

那麼,什麼是 SOLID 原則呢?它為什麼會被經常提到呢?維基百科 - SOLID 是這樣描述的:“在物件導向的計算機程式設計中,術語 SOLID 是對五大軟體設計原則的助記縮寫,這五個原則旨在讓軟體設計更易理解、更靈活、更易於維護。它與 GRASP 軟體設計原則並無關係。這些軟體設計原則是 Robert C. Martin 提出的眾多原則中的一個子集。雖然它們適用於任何物件導向的設計,但是 SOLID 原則也可以形成諸如敏捷開發或者自適應軟體開發等方法論的核心理念。Martin 在 2000年 的論文《設計原則與設計模式》中介紹了 SOLID 原則的理論。”。

如果正確地遵循了 SOLID 原則,那麼它將引導你寫出維護性和可測試性更好的高質量程式碼。如果需求變更,你也可以輕鬆地修改程式碼(與不遵守 SOLID 的程式碼相比)。我認為向你提及這些內容是非常重要的:這些原則關注的是可維護性、可測試性以及和人類思維相關的更高質程式碼,而不是你為之編碼的計算機。工作在以效能為主的領域的開發者各自有著不同的程式設計方法。由於在我們生活的世界裡,人工時間成本比機器時間成本更昂貴,所以大多數開發者都不在高效能需求的領域裡工作,而且他們常常被鼓勵去使用物件導向的程式設計方法。在這種情況下,大部分開發者都能很好地應用 SOLID 原則。

從本文開始,我將按照單詞首字母縮寫的順序盡力向你解釋每一個原則。為了簡潔起見,我會把這個主題分為 3 個部分。本文是我將向你介紹的前兩個原則的第 1 部分。好了,讓我們從字母 S 開始吧。

S

SOLID 中的 “S” 表示的是單一職責原則(Single Responsibility Principle,簡稱 SRP),它是最容易理解也可能是最容易讓人忽視的一個原則。此原則意味著一個類應該只做且只能做一件事情。如果一個類未能達到它的目的,那麼你就可以看著它並說“你做了一件事……”。

【譯】什麼是SOLID原則(第1部分)

舉例來說,假如我們需要從網路上獲取一些 JSON 資料,然後解析它,並把結果儲存在本地資料庫中。根據我們正在編碼的平臺,這種工作可以使用為數不多的程式碼來實現。由於程式碼量不多,我們可能會想把所有的邏輯全部扔到一個類中。但是,根據單一職責原則,這將會是一個糟糕的做法。我們可以清楚地區分 3 個不同的職責:從網路上獲取 JSON 資料,解析資料,儲存解析的結果到資料庫中。基於此,我們應該有 3 個類。

第 1 個類應該只處理網路。我們給它提供一個 URL,然後接收 JSON 資料或者在出現問題時,收到一個錯誤資訊。

第 2 個類應該只解析它接收到的 JSON 資料並以相應的格式返回結果。

第 3 個類應該以相應的格式接收 JSON 資料,並把它儲存在本地資料庫中。

【譯】什麼是SOLID原則(第1部分)

為什麼非要這麼麻煩呢?通過這樣分離程式碼,我們能獲得什麼好處呢?其中一個好處就是可測試性。對於網路請求類,我們可以使用一個測試的 URL 分別在請求成功和發生錯誤的測試用例下來觀察它的正確行為。為了測試 JSON 模組,我們可以提供一個模擬的 JSON 資料,然後檢視它生成的正確資料。同樣的測試原則也適用於資料庫類(提供模擬資料,在模擬的資料庫上測試結果)。

有了這些測試,如果我們的程式出了問題,我們可以執行測試並檢視問題發生在哪個地方。可能是伺服器上的某些部分發生了改變,導致我們接收了有損資料。或者資料是正常的,但是我們在 JSON 解析模組中遺漏了什麼,致使我們不能正確地解析資料。又或者可能我們正嘗試插入資料的資料庫中不存在某個列。通過這些測試,我們不必猜測問題出在哪個地方。看到了問題所在,我們就努力地去解決它。

除了可測試性,我們還擁抱了模組化。如果專案需求變更,伺服器返回資料的格式是 XML 或者其他的自定義格式而非 JSON,那麼我們所要做的就是編寫一個處理資料解析的新模組,然後用這個新的代替 JSON 模組。或者可能因為一些奇葩的理由,上述兩者我們都需要,而網路模組再根據一些規則呼叫正確的模組。

如果我們有一些本地的 JSON 檔案需要解析並將解析的資料發給其他模組時該怎麼辦呢?那麼,我們可以把本地的 JSON 傳送給我們的解析模組,然後獲取結果並把它用在需要的地方。如果我們需要以 Flat 的格式(譯者注:Flat File)而不是資料庫的形式本地儲存資料呢?同樣沒問題,我們可以用一個新的模組來替換資料庫模組。

如你所見,這個看似簡單的原則有很多優勢。通過遵守這個原則,我們已經能夠想象的到我們的程式碼庫在可維護性方面會有重大改進。

O

【譯】什麼是SOLID原則(第1部分)

字母“O”表示的是開閉原則( Open-Closed Principle,簡稱 OCP)。常言道,我們的類應該對擴充套件開發,對修改關閉。什麼意思呢?我的理解是,我們應該以外掛式的方式來編寫類和模組。如果我們需要額外的功能,我們不應該修改類,而是能夠嵌入一個提供這個額外功能的不同類。為了解釋我的理解,我將使用一個經典的計算器示例。這個計算器在最開始只能執行兩種運算:加法和減法。計算器類看起來像下面這樣(本文中的程式碼不是用特定語言編寫的):

class Calculator {
  public float add(float a, float b) {
    return a + b
  }
  public float subtract(float a, float b) {
    return a — b
  }
}
複製程式碼

我們像下面這樣使用這個類:

Calculator calculator = new Calculator()

float sum = calculator.add(10, 2) //the value of sum is 12
float diff = calculator.subtract(10, 2) //the value of diff is 8
複製程式碼

現在,我們假設客戶希望為這個計算器新增乘法功能。為了新增這個額外的功能,我們必須編輯計算器類並新增乘法方法:

public float multiply(float a, float b) {
  return a * b
}
複製程式碼

如果需求又一次改變,客戶又需要除法,sincospow以及眾多的其他數學函式,我們不得不一次又一次編輯這個類來新增這些需求。根據開閉原則,這並不是一個明智的做法。因為這意味著我們的類可以修改。我們需要讓它遮蔽修改,而對擴充套件開放,那麼我們該怎麼做呢?

首先,我們定義一個名為 Operation 的介面,這個介面只有一個名為 compute 的方法:

interface Operation {
  float compute(float a, float b)
}
複製程式碼

之後,我們可以通過實現 Operation 介面來建立操作類(本文中提供的大多數示例也可以通過繼承和抽象類來完成,但我更喜歡使用介面)。為了重建簡單的計算器示例,我們將編寫加法和減法類:

class Addition implements Operation {
  public float compute(float a, float b) {
    return a + b
  }
}
class Subtraction implements Operation {
  public float compute(float a, float b) {
    return a — b
  }
}
複製程式碼

我們的新計算器類只有一個名叫 calculate 的方法,在這個方法中,我們可以傳遞運算元與操作類:

class Calculator {
  public float calculate(float a, float b, Operation operation) {
    return operation.compute(a, b)
  }
}
複製程式碼

我們將像下面這樣使用我們的新類:

Calculator calculator = new Calculator()

Addition addition = new Addition()
Subtraction subtraction = new Subtraction()

float sum = calculator.calculate(10, 2, addition) //the value of sum is 12
float diff = calculator.calculate(10, 2, subtraction) //the value of diff is 8
複製程式碼

現在如果我們需要新增乘法,我們將建立這樣的一個乘法運算類:

class Multiplication implements Operation {
  public float compute(float a, float b) {
    return a * b
  }
}
複製程式碼

然後通過新增以下內容在上面的示例中使用它:

Multiplication multiplication = new Multiplication()
float prod = calculator.calculate(10, 2, multiplication) // the value of prod is 20
複製程式碼

【譯】什麼是SOLID原則(第1部分)

我們終於可以說我們的計算器類對修改關閉,對擴充套件開放了。看一下這個簡單的例子,你可能會說將這些額外的方法新增到原始的計算器類中也沒什麼大問題,還有就是可能更好的實現也就意味著編寫更多的程式碼。誠然,在這個簡單的情景中,我更贊同你的說法。但是,在現實生活裡的複雜情景下,遵守開閉原則編碼將大有裨益。也許你需要為每個新功能新增遠不止那三個方法,也許這些方法非常複雜。然而通過遵循開閉原則,我們可以用不同的類外化新的功能。它將有助於我們以及他人更好地理解我們的程式碼,而這主要是因為我們必須專注於較小的程式碼塊而不是滾動無休止的檔案。

為了更好地視覺化這個概念,我們可以把計算器類視為第三方庫的一部分,並且無法訪問其原始碼。好的實現就是編寫它的善良的人們遵守了開閉原則,使它對擴充套件開放。因此,我們可以使用自己的程式碼擴充套件其功能,並輕鬆地在我們的專案中使用它。

如果這聽起來仍讓人犯傻,那就這樣想吧:你剛剛為客戶編寫了一個很棒的軟體,它完成了客戶想要的一切。你盡最大能力編寫了所有的內容,並且程式碼質量令人驚歎。數週後,客戶想要新的功能。為了實現它們,你必須潛心投入到你的漂亮程式碼中,修改各種檔案。這樣做,有可能程式碼質量會受到影響,特別是在截止日期緊張時。如果你已經為你的程式碼編寫了測試(這也是你應該做的),那麼這些修改可能會破壞一些測試,你還必須修改這些測試。

這與遵守了開閉原則編寫的程式碼形成了鮮明的對比。要實現新功能,你只需編寫新程式碼即可。舊程式碼保持不變。你所有的舊測試仍然有效。因為我們不是生活在一個完美的世界中,所以在某些跑偏的情況下,你可能仍然會對舊程式碼的某些部分進行細微的更改,但這與非開閉原則帶來的修改相比則可以忽略不計。

除此之外,遵循開閉原則的編碼方式還能讓你在心理上獲得極大的愉悅體驗。其中一個就是,你只需要編寫新程式碼,而無須為了實現新功能對你引以為傲的程式碼痛下殺手。通過為新功能編寫新程式碼,而不是修改舊程式碼,高漲的團隊士氣將隨之而來。這可以提高生產效率,從而減少工作壓力,改善生活質量。

【譯】什麼是SOLID原則(第1部分)

我希望你能看到這個原則的重要性。不過令人沮喪的是,在一個真實的專案中,主要是由於缺乏魔法水晶球的能力,我們很難預見未來以及應該如何、在哪裡應用這個原則。但是,知道了開閉原則的確有助於在需求來臨時識別出可能的用例。在一開始的實現中,當客戶想要給這個計算器新增乘法和除法功能時,我們隨手將這兩個方法新增到了 Calculator 類中。接下來,當他還要 sincos 時,我們也許會對自己說:“等會兒……”。等待過後,我們開始重構程式碼以適配開閉原則來避免將來可能遇到的麻煩。現在,當客戶還想要 tanpow 以及其他功能時,我們早就搞定了。

……

你可以在 什麼是SOLID原則(第2部分) 閱讀下兩個 SOLID 原則。如果你喜歡這篇文章,你可以在我們的 官方站點 上找到更多資訊。

相關文章