設計模式之禪 1. 單一職責原則 2. 里氏原則 3. 依賴倒置 4. 接口隔離 5 .迪米特法則 6. 開閉原則 (重中之重) 總結:

前言:
讀《設計模式之禪》有感,這裏提煉一些設計原則加上自己理解,爭取通俗易懂,希望能闡述作者思想一二,感謝作者!

-- 書中雖是java語言做的示例(插圖),但是原理相通,iOS童鞋也不必擔心基本能看懂,有些和OC差別較大的名詞我也做了註釋,不影響對原則的理解!

內容包含:六大設計原則23種設計模式各模式間VS擴展篇...等五大模塊,建議先從設計原則入手,設計原則可看作爲內功心法,而設計模式則作爲武學招式,需內外兼修。對原則很好的理解,再去看23種模式時,會發現處處透露出這六大原則的身影!
這篇先說下六大原則:

目錄 (六大原則)

  • 單一職責(只有一個原因使類發生變更)
  • 里氏替換(繼承)
  • 依賴倒置 (具體類、抽象類)
  • 接口隔離(接口細化)
  • 迪米特法則(知道的越少越好)
  • 開閉原則(對擴展開放,對修改關閉)

1. 單一職責原則

單一職責原則的英文名稱是Single Responsibility Principle,簡稱是SRP
定義:有且只有一個原因使類發生變更

這個原則備受爭議,主要爭議之處在於對“職責”的定義

1.1 什麼是職責?

職責不可度量,因需求而異。

舉個例子:
一個用戶對象,其中包含有用戶的信息和行爲,這樣的一個用戶類接口如下:


這樣代碼有什麼問題嗎?滿足一個對象的所有方法,
但是若另一種定義來說,UserInfo類中包含用戶的屬性和行爲, 兩者任一的改動都會引起當前userInfo類的改動, 從嚴格上來看,並不符合單一職責原則,這樣修改呢

IUserBo 負責屬性: 只有用戶屬性修改才使當前類發生變化
IUserBiz 負責行爲:只有用戶行爲變化才使當前類發生變化,

符合我們所說的單一職責原則,這樣說:職責定義不同,導致我們的業務模塊拆分也不同

1.2 如何分清職責?

在這裏再擴展下定義:
單一職責原則要求一個接口或類只有一個原因引起變化,也就是一個接口或類只有一個職責,它就負責一件事

舉個:
修改一個IUserManager類的接口:這個接口change User 有很多原因使其更改, 比如: userName 、 HomeAddress 、 officeTel...等


如果修改這樣呢

每個接口職責分明,結構清晰

單一職責原則有什麼好處:

類的複雜性降低,實現什麼職責都有清晰明確的定義;
可讀性提高,複雜性降低,那當然可讀性提高了;
可維護性提高,可讀性提高,那當然更容易維護了;
變更引起的風險降低,變更是必不可少的,如果接口的單一職責做得好,一個接口修改只對相應的實現類有影響,對其他的接口無影響,這對系統的擴展性、維護性都有非常大的幫助。

1.3 活用活用,不要教條主義

翻看很多開源的框架就類而言很少滿足單一原則的,比如 userInfo 中拆分兩個類,我們勢必要維護兩個相同的生命週期,另外完全按照單一原則來分類,可能劃分出多個類來,人爲的增加複雜性和維護成本,我們不奉行教條主義;
所以對於單一職責原則, 我的建議是接口一定要做到單一職責,類的設計儘量做到只有一個原因引起變化.

注意: 單一職責原則提出了一個編寫程序的標準,用“職責”或“變化原因”來衡量接口或類設計得是否優良,但是“職責”和“變化原因”都是不可度量的,因項目而異,因環境而異。

2. 里氏原則

定義: 所有引用基類的地方必須透明地使用其子類的對象
通俗點講:子類完全可以替代父類,反之不成立,主要爲繼承量身打造

2.1 熟悉場景

面嚮對象語言,我們用過繼承
它有如下優點:

  • 代碼共享,減少創建類的工作量,每個子類都擁有父類的方法和屬性;
  • 提高代碼的重用性;
  • 子類可以形似父類,但又異於父類,“龍生龍,鳳生鳳,老鼠生來會打洞”是說子擁有父的“種”,“世界上沒有兩片完全相同的葉子”是指明子與父的不同;
  • 提高代碼的可擴展性,實現父類的方法就可以“爲所欲爲”了,君不見很多開源框架的擴展接口都是通過繼承父類來完成的;
  • 提高產品或項目的開放性。

自然界的所有事物都是優點和缺點並存的,即使是雞蛋,有時候也能挑出骨頭來,繼承的缺點如下:

  • 繼承是侵入性的。只要繼承,就必須擁有父類的所有屬性和方法;
  • 降低代碼的靈活性。子類必須擁有父類的屬性和方法,讓子類自由的世界中多了些約束;
  • 增強了耦合性。當父類的常量、變量和方法被修改時,需要考慮子類的修改,而且在缺乏規範的環境下,這種修改可能帶來非常糟糕的結果——大段的代碼需要重構。

2.2 定義解讀

里氏替換原則爲良好的繼承定製了一個規範,
不要小看一句簡單的定義確包含了4層含義:

  • 2.2.1 子類必須完全實現父類的方法

舉個:
玩過CS遊戲裏,槍支類圖


抽象基類AbstractGun作爲槍支,具備shoot()射擊功能
如果加入一種玩具槍(不具備射擊功能)呢,要不要還繼承這個抽象基類呢,繼承好像也沒問題,可以不調用或覆寫shoot(),使其不具備射擊功能,但是不建議這樣做,更合理方式類圖:

注意 如果子類不能完整地實現父類的方法,或者父類的某些方法在子類中已經發生“畸變”,則建議斷開父子繼承關係,採用依賴、聚集、組合等關係代替繼承。

  • 2.2.2 子類可以有自己的個性

子類可以有自己的方法和屬性,相應子類可以勝任父類,但是父類不可以勝任子類

  • 2.2.3 覆蓋或者實現父類的方法時輸入參數被放大

父類

public class Father {     
public Collection doSomething(HashMap map){         
   System.out.println("父類被執行...");           
 return map.values();   
 }}

子類

public class Son extends Father {    
 //放大輸入參數類型     
public Collection doSomething(Map map){          
   System.out.println("子類被執行...");            
 return map.values();   
  }}

子類與父類方法名相同,參數範圍不同,這不是覆寫算作重載, 子類輸入參數範圍大於父類,子類調用doSomething會執行父類方法,如果我們想執行子類方法必須重寫,符合我們想要方式,但是如果父類輸入參數範圍大於子類呢,
請記住一句話:有父類的地方,子類應該完全勝任, 我們調用子類的方法,不走父類方法,不通過覆寫方式,就實現了子類不走父類,這和我們常用邏輯不符,有可能我們實際場景中調用方法是想要走父類方法,但是效果走了子類方法

2.2.4 覆寫或實現父類的方法時返回結果可以被縮小

當父類返回一個類型T,子類的相同方法(覆寫或重載)返回值爲S,里氏替換原則要求S必須小於等於T,也就是說要麼S和T一個類型,要麼S是T的子集,爲什麼,在默唸這一句話: 有父類的地方,子類應該完全勝任

3. 依賴倒置

原始定義是:High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.
翻譯過來,包含三層含義:
高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象;
抽象不應該依賴細節;
細節應該依賴抽象。
簡單的例圖:
一個司機開着一輛奔馳轎車
Driver 作爲司機類
Benz 作爲汽車類


那如果司機在買一輛寶馬呢?我們會重新創建個寶馬類,依次增加嗎? 他們具有抽象的共性,都是汽車,都能run(), 此時我們應該建立抽象類ICar, 奔馳和寶馬性能不同車型不同,再由具體類來細化.

總結:
依賴倒置原則的本質就是通過抽象(接口(OC中類似協議)或抽象類)使各個類或模塊的實現彼此獨立,不互相影響,實現模塊間的松耦合; 可以遵循以下規則:

  • 每個類儘量都有接口或抽象類,或者抽象類和接口兩則兼備
  • 變量的表面類型儘量是接口或抽象類
  • 任何類都不應該是從具體類派生過來
  • 儘量不要覆寫基類方法
  • 結合里氏替換原則使用

4. 接口隔離

定義: 客戶端不應該依賴他不需要的接口
是不是有點不好理解,來看它的第二種定義: 類間的依賴關係應該建立在最小的接口上

  • 那什麼是接口?
    這裏接口可以理解爲一個實例類對象的實例接口和類接口,Java開發的是不是有點疑惑,換個角度來看,Java中的類也是一種接口,這話是作者說的不是我說的,注意磚頭方位,作爲OC開發更容易接受這個概念
  • 那我們如何是最小接口呢?
  1. 接口儘量小
    就是和上文提到的單一職責像呼應,儘量職責單一

  2. 接口高內聚,
    比如你的領導讓你做一件事,這件事可能要分a、b、c完成,你只需要彙報領導任務完成,領導不需要知道過程細節,結合單一職責來說a、b、c應該劃分單獨事件,但是對外只需要提供完成任務接口就行;高內聚,儘量少暴露公開;對於屬性對外只讀,決不給讀寫權限,公開方法也是,減少外部影響,接口是對外的承諾,承諾越少對系統開發越有利

  3. 模塊定製
    爲個別業務提供定製接口,減少對全部接口的訪問
    舉個栗子:
    一個圖書館內查詢系統,我們提供類具有按照作者、標題、出版社、等分類查詢和混合查詢方式


這是其他業務方不清楚使用,每次查詢都適用混合查詢方式,導致系統速度異常慢,做個隔離,爲業務方提供定製接口


  1. 接口設計有限度
    接口的設計粒度越小,系統越靈活,這是不爭的事實,但是靈活也帶來結構的複雜化,開發難度增加,可維護性降低,所以接口設計一定要注意適度,這個“度”如何來判斷呢?根據經驗和常識判斷,沒有一個固化或可測量的標準。

5 .迪米特法則

定義: 也爲最少知道原則,一個對象應該對其他對象有最少的瞭解,

通俗講:一個類應該對自己需要耦合或調用的類的內部知道的最少,被調用的類內部如何複雜都和我沒關係,我就知道你提供的這麼多Public方法,我就關心調用這麼多,其他一概不關心
其中主要包含3層含義:

  • 只有朋友交流
    最少朋友思維,不必要和朋友的朋友都認識,只要認識你這個朋友,你的朋友會找到他的朋友幫你辦好事。
    只對自己必然要聯繫對象進行關聯,不必要的對象減少耦合,不應和過多對象建立關係,如果過多就該考慮如何分出管理了,“儘量做到滿身筋骨,而不是肥嘟嘟!”

  • 朋友間應該保持適當距離
    即使關聯類之間,也應該保持相應“距離”, 不能無所不知,不需要完全暴露所有細節,這就是前面說的高內聚,只提供公共方法,具體實現對外不需要暴露,
    注意: 迪米特法則要求類“羞澀”一點,儘量不要對外公佈太多的public方法和非靜態的public變量,儘量內斂,多使用private訪問權限。

  • 自己的還是自己
    在實際應用中經常會出現這樣一個方法:放在本類中也可以,放在其他類中也沒有錯,那怎麼去衡量呢?你可以堅持這樣一個原則:如果一個方法放在本類中,既不增加類間關係,也對本類不產生負面影響,那就放置在本類中。

總結: 迪米特法則的核心觀念就是類間解耦,弱耦合,只有弱耦合了以後,類的複用率纔可以提高。其要求的結果就是產生了大量的中轉或跳轉類,導致系統的複雜性提高,同時也爲維護帶來了難度。

6. 開閉原則 (重中之重)

核心重點終於來了,就是開閉原則,哲學上說矛盾法則是唯物辯證的最根本法則,那開閉原則,可以作爲最基礎的設計原則。
定義:
Software entities like classes,modules and functions should be open for extension but closed for modifications.(一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。)
我最喜歡這一句話:對修改關閉,對擴展開放
開閉原則告訴我們應儘量通過擴展軟件實體的行爲來實現變化,而不是通過修改已有的代碼來完成變化,它是爲軟件實體的未來事件而制定的對現行開發設計進行約束的一個原則。

舉個例子吧
以一個書店銷售小說書籍爲例:
IBook作爲抽象類,定義了數據的三個屬性:姓名 、價格、作者
NovelBook 爲具體子類, 獲取小說書信息

若此時書店搞活動打折處理,我們怎麼修改上面類呢,我們會早NovelBook類getPrice()中加入打折信息,其實不建議,想到那句話: 對修改關閉,對擴展開放, 符合開閉原則來說,我們對原有NovelBook類不應進行修改,而是擴展一個打折OffnoveBook子類, 結構如下:


如果後期加入其他書籍銷售呢,比如計算機類書籍,我們也是隻需要在原有基礎上擴展新的子類即可,結構如下:

總結:開閉原則是個很抽象感念,也是很虛的概念,它定義簡單,但又不簡單,我們在一直擁抱變化,試想如何讓其保持遵循開閉原則。

使用開閉原則需要注意什麼

  • 開閉只是一個原則,口號實現擁抱變化的方法很多,前提條件是:類必須做到高內聚,低耦合,這樣擁抱變化時減少不可預料故障
  • 項目規章非常重要, 有個穩定的規章,也是所有成員必須遵守的約定,能給我們帶來非常多的好處,如提高開發效率,降低缺陷率,減少維護成本等
  • 預知變化
    在實戰過程中,架構師或項目經理一旦發現有發生變化的可能,則需要考慮現有架構能否輕鬆適用這一變化。架構師設計的一套系統不僅要符合現有的需求,也要適應可能發生的變化。

開閉原則是一個終極目標,任何人包括大師級人物都無法百分之百做到,但朝這個方向努力,可以非常顯著地改善一個系統的架構,真正做到“擁抱變化”。

總結:

軟件設計痛苦之處在於應對需求的變化,但是需求有不可預估,要求我們爲不可預估的需求做好準備。
大師們總結的6大設計原則和23種設計模式,是來幫助我們“擁抱”未來的變化, 但它不是教條主義,成爲限制我們的邊框;靈活使用,因地而異,有可能幾大原則之間會存在場景衝突的時候,我們應有自己理解使用!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章