天天看點

架構設計原則之我見(二):SOLID 原則

雲栖号資訊:【 點選檢視更多行業資訊

在這裡您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!

SOLID 原則,據 WikiPedia 所說,是由 Robert C. Martin 總結的面向對象設計原則。這個名字其實是以下五個原則的首字母簡寫:

  • Single responsibility principle;
  • Open/closed principle;
  • Liskov substitution principle;
  • Interface segregation principle;
  • Dependency inversion principle。

“Single responsibility principle”

這句話翻譯成中文是“單一職責原則”。這是一句缺乏主語的話,推斷應該是指設計師所設計的系統吧。是以補充完整後,整句話的意思應該是:“設計師所設計的目标系統,其職責應該是單一的”。

如何判定“職責”是否“單一”?

判定“職責單一”的标準是什麼難以回答,隻能通過作者的文章進一步分析,嘗試了解作者原意。

這個原則也并非 SOLID 原則作者原創,據作者原文所說:“This principle was described in the work of Tom DeMarco and Meilir Page-Jones . They called it cohesion”,原來這個原則來源于 Tom DeMarco 和 Meilir Page-Jones 兩位前輩的工作,原本叫做“Cohesion”,也就是“内聚”。作者對“内聚”給出的解釋是:“A class should have only one reason to change”。下文根據作者所給出的例子,來進一步了解作者的意圖。

文章開頭以一個保齡球遊戲的程式設計設計來探讨這一原則。原本 Game 類有兩個責任:一、負責跟蹤目前幀,相當于打球;二、負責計算分數。作者認為,如果把這兩個職責放在同一個類中,會引起耦合,是以要對 Game 作架構拆分,把這兩個責任分别拆分給兩個不同的類,并給出了拆分的理由:“Because each responsibility is an axis of change”,意思是“因為每個職責都是一個變化的次元”。猜想作者想表達的是,由于這兩個職責是互相正交的次元,分拆開後,可以避免它們互相影響的意思。

這裡其實有兩個問題:

首先,兩個職責放在同一個類中,并不代表會發生耦合。

耦合的意思是當一個職責内部發生變動時,會影響到另外一個職責的正常執行。假設把兩個職責的代碼糅合在一起,形成一個大的代碼塊,這當然是耦合的,此時修改任何一個職責都要小心,牽一發而動全身。

但是我們可以把這兩個職責放在兩個不同的方法中,比如拆分成 Game.trackFrame(), Game.calcScore() 兩個方法後,在修改其中一個職責時,隻要輸入輸出的參數不發生變化,也并不會産生耦合。也就是說,要解決耦合這一問題,并非隻有“拆分成兩個不同的類”這一個解決方案,在同一個類中拆分成兩個方法也可以解決,因為拆分成方法是拆分成類的前提。是否需要拆分成類,還需要有其他方面的考慮,解耦這一理由還不夠充分,此處就不詳細展開。

其次,很多人都忽略了為何兩個職責可以被拆分開。

我們需要回到現實生活來分析保齡球遊戲的核心生命周期。

在現實生活中打保齡球時,确實有算分這一環節。在每一次打球結束時, 機器會自動給出分數。當然,在早期沒有機器時,這個分數肯定是由打球人自己來算的。為什麼後來可以拆分出來交給機器來算呢?因為算分活動必須等待打球結束才能進行,打球與算分二者在執行時間上是屬于完全不會發生交叉的兩個連續動作,且打球的結果作為算分的輸入,是以兩個動作本來就是沒有耦合的,可以拆分開,成為保齡球遊戲生命周期中的兩個相續活動。

這兩個活動哪一個才是核心生命周期活動呢?可以看到,人們去保齡球館是為了親身體驗打球,而不是為了體驗得分。而且即使沒有算分規則,人們也 可以玩的很開心,但如果沒有打球的體驗,隻有算分規則,那麼這個遊戲也就不成立了。是以,這個遊戲的核心生命周期是打球,而非算分。算分隻是在打球結束後對結果的計算,屬于非核心生命周,是以分數計算規則代碼可以從打球代碼中拆分出來,以保齡球遊戲所産生的結果作為算分的輸入來推動執行,形成樹狀結構。

而在拆分後,Game 的原本功能并沒發生任何變化,隻不過将其中一個步驟的實作代碼分離出去了而已,然後通過方法調用,以直接擷取結果的方式整合回歸,還是同一個整體,沒有發生變化。這一做法,使得 Game 能夠更加專注于其本身的職責,分數計算自身也能更加專注,各自被修改時也可以互不影響。

是以,二者能夠拆分開,并非“Because each responsibility is an axis of change”,而是因為其中存在非核心生命周期活動。并且拆分也并不僅限于拆分成類,首先應該能拆分成方法,這是拆分為類的前提。

“單一”與“内聚”

再從這個例子來分析“單一”的含義,确實還是叫“内聚”比較好。

從内聚的角度來看,在打球和算分兩個方法拆分開後,trackFrame() 與 calcScore() 各自都專注于自身的業務,不受對方的影響,是以二者都是内聚的,自身都是完整的,隻要給出輸入參數就可以獨立傳回輸出結果。而且 Game 這個類完整包含了保齡球自身的業務,其自身也是内聚的。

可是一旦改成“單一職責”,意思就發生了變化,着重點變成了“單一”。其後文在詳細解釋時,又把表述從“an axis of change”改成“one reason to change”, 意思進一步發生了變化:“an axis of change”指的是一個次元,而“one reason to change”指的是一個理由。二種表述差別很大,完全誤解了“内聚”的本意,難怪會有很大的争議。

另外怎樣才能算“職責單一”呢?這是沒有确定标準的,需要相對于某個一個參考點才能确定是否單一。比如 Game 包含打球和算分兩個步驟,難道 Game 的職責就不“單一”了嗎?不是的。保齡球遊戲需要打球和算分兩個步驟,以組成一個“單一”的運動,放在一起正是為“單一”運動而服務的,這樣做并不能說不“單一”。隻有把對比的對象改為打球和算分時,才可以說 Game 的職責不單一。但是打球和算分本身就是從 Game 中拆分出來的,怎麼可以拿整體相對其拆分出來的部分來比“單一”呢?這不合理。如果真的這麼去比,即使把打球和算分二者拆分開後,算分的職責就“單一”了嗎?也不是的,算分也可以拆分為很多不同的規則,在規則的層面看,算分的職責也并不“單一”,還需要再拆分!按照這個“單一職責”分拆下去,永遠沒有止境,陷入死循環。

是以“單一”是一個相對的詞語,必須要看針對什麼來說是“單一”的,不能單獨來看。也不能因為一個事情分為兩個步驟,就說這個事情不“單一”,因為這兩個步驟所組成的是同一個事情,是單一的。而把這兩個步驟拆分開後由兩個人來分别執行,對于這兩個人來說,各自的職責仍然是單一的,但是不能是以而否認二者所組成的原來那個事情不“單一”。正因為這兩個人各自“單一”職 責的完成,組成了原本的那個“單一”的事情。

回過頭來,如果讀者明白“内聚”,站在“内聚”的角度來看“單一職責”原則, 來了解作者的“A class should have only one reason to change”這個解釋,就可以秒懂作者隻不過是想表達“内聚”而已。是以,讀者千萬不要真的從“單一職責”的角度去了解這個原則,會很容易産生誤解,作者不過是想通過這一原則來表述作者所了解的“内聚”含義罷了。

掌握”内聚“,才是根本!

【雲栖号線上課堂】每天都有産品技術專家分享!

課程位址:

https://yqh.aliyun.com/zhibo

立即加入社群,與專家面對面,及時了解課程最新動态!

【雲栖号線上課堂 社群】

https://c.tb.cn/F3.Z8gvnK

原文釋出時間:2020-05-08

本文作者:王概凱

本文來自:“

InfoQ

”,了解相關資訊可以關注“