1. 程式人生 > >設計模式與設計原則簡介(一)

設計模式與設計原則簡介(一)

  什麼是設計模式?

我們知道對於很多數學問題,經常會有多種不同的解法

而且這其中可能會有一種比較通用簡便高效的方法

我們在遇到類似的問題或者同一性質的問題時,也往往採用這一種通用的解法

將話題轉移到程式設計中來

對於軟體開發人員, 在軟體開發過程中, 面臨的一般問題的解決方案就是設計模式(準確的說是OOP中)

當然,如同數學的解題思路一樣,設計模式並不是公式一樣的存在

設計模式(Design pattern)代表了最佳的實踐

是眾多軟體開發人員經過相當長的一段時間的試驗和錯誤總結出來的寶貴經驗

是解決問題的思路

總之,設計模式是一種

思想思想思想


起源

隨著面向物件程式語言的發展,以及軟體開發規模的不斷擴大

編寫良好的OOP程式變得困難,而編寫可複用的OOP程式則更是困難

在 1994 年,由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides

四人合著出版了一本名為 Design Patterns - Elements of Reusable Object-Oriented Software(中文譯名:設計模式 - 可複用面向物件軟體的基礎) 的書

該書首次提到了軟體開發中設計模式的概念。

四位作者合稱 GOF

(四人幫,全拼 Gang of Four)

這就是設計模式四個字的起源

當然,即使在這本書出版之前,肯定也已經有很多有經驗的OOP程式設計師已經在使用自己的經驗(設計模式)了

但是這本書將OOP的設計經驗作為設計模式記錄下來

使我們能夠更加簡單方便的複用成功的設計經驗和體系結構

 

設計原則

"隨著面向物件程式語言的發展,以及軟體開發規模的不斷擴大

編寫良好的OOP程式變得困難,而編寫可複用的OOP程式則更是困難"

設計模式的起源, 正是需要設計模式的根本原因

藉助於設計模式,可以更好地實現程式碼的複用,增加可維護性

 

怎麼才能更好地實現程式碼複用呢?

面向物件有幾個原則:

根本原則

 

開閉原則(Open Closed Principle,OCP)  一個軟體實體應當對擴充套件開放,對修改關閉 。 軟體實體應儘量在不修改原有程式碼的情況下進行擴充套件

 

在開閉原則的定義中,軟體實體可以指一個軟體模組、一個由多個類組成的區域性結構或一個獨立的類

不修改已有程式碼的基礎上擴充套件系統的功能的形式,就是符合開閉原則的

開閉原則的關鍵是抽象

比如,一個方法中

if(){

//...

}else if(){

//...

}

如果新增加一個邏輯功能點,則需要增加新的else  或者 else if ,勢必修改了已有程式碼

而如果面向抽象的介面或者抽象類進行程式設計,擴充套件增加新的功能,只需要傳遞新的子類即可,原有的程式碼功能不會有任何的修改

 

再比如

實際專案開發的時候,我們會把一些配置寫入到配置檔案中,而不是"硬編碼"到程式碼中

修改引數設定的時候,原始碼無需更改,這也是符合開閉原則

開閉原則作為根本原則,並不限定某種具體場景,只要是符合了這一含義,就是符合開閉原則

總之,開閉原則就是別因為新增功能擴充套件改(老)程式碼

 

 

六大原則

開閉原則是根本綱領,它是面向物件設計的終極目標

 

除了根本原則另外還有六大原則 , 則可以看做是開閉原則的實現方法

 

  • 單一職責原則 (Single Responsiblity Principle SRP)
  • 里氏替換原則(Liskov Substitution Principle,LSP)
  • 依賴倒轉原則(Dependency Inversion Principle,DIP)
  • 介面隔離原則(Interface Segregation Principle,ISP)
  • 合成/聚合複用原則(Composite/Aggregate Reuse Principle,C/ARP)
  • 迪米特法則(Principle of Least Knowledge,PLK,也叫最小知識原則)

 

單一職責原則 (Single Responsiblity Principle SRP)

一個類只負責一個功能領域中的相應職責,或者可以定義為:就一個類而言,應該只有一個引起它變化的原因

單一職責的原則很簡單,就是一個實體(一個類或者一個功能模組)不要承擔過多的責任

承擔了過多的責任也就意味著多個功能的耦合

堆積木時, 到底是一塊積木比較容易利用, 還是多塊積木拼接起來的"一大塊" 更容易利用? 結果顯而易見 

而且,承擔了過多的責任,也就是可能會因為多個原因修改這段程式碼

隨之而來的是不穩定性以及維護成本的增加,也就是將會有多個原因引起他變化

單一職責原則的根本在於控制類的粒度大小

 

里氏替換原則(Liskov Substitution Principle,LSP)

里氏替換原則是以提出者 Barbara Liskov  的名字命名的 

定義:

如果對每一個型別為 T1的物件 o1,都有型別為 T2 的物件o2

使得以 T1定義的所有程式 P 在所有的物件 o1 都代換成 o2 時,程式 P 的行為沒有發生變化

那麼型別 T2 是型別 T1 的子型別

 

簡單說就是 如果 一個程式P(T1) ,如果將輸入T1 替換為T2 ,而且 P(T1) = P(T2)

那麼T2 是T1的子型別

 

再簡單的概述就是:

所有引用基類的地方必須能透明地使用其子類的物件

 

透明也就意味著不感知,不受任何影響

聽起來好像很自然的就可以做到

假如子類覆蓋了父類的方法呢?假如子類覆蓋了父類的方法並且改變了父類方法的原有功能邏輯呢?

比如,原來傳遞來兩個引數進行加法運算,子類覆蓋後,進行減法運算,會發生什麼?

里氏代換原則的根本,在軟體中將一個基類物件替換成它的子類物件,程式將不會產生任何錯誤和異常

想要透明的使用子類,滿足里氏替換原則

需要注意應該儘可能的將父類設計為抽象類或者介面

讓子類繼承父類或實現父介面,並實現在父類中宣告的方法,這樣可以做到滿足開閉原則

子類的所有方法必須在父類中宣告,或子類必須實現父類中宣告的所有方法,也就是父類定義,子類實現

而且,子類不應該破壞父類的契約,也就是不能更改原有的方法的邏輯含義

里氏替換是繼承複用的基石,只有當子類可以替換父類,且軟體單位的功能不受到影響時

父類才能真正被複用,而子類也能夠在基類的基礎上增加新的行為

里氏代換原則是對開閉原則的補充。

實現開閉原則的關鍵步驟就是抽象化,而基類與子類的繼承關係就是抽象化的具體實現,所以里氏代換原則是對實現抽象化的具體步驟的規範

 

依賴倒轉原則(Dependency Inversion  Principle, DIP)

抽象不應該依賴於細節,細節應當依賴於抽象; 換言之,要針對介面程式設計,而不是針對實現程式設計

 

也就是使用介面和抽象類進行變數型別宣告、引數型別宣告、方法返回型別宣告,以及資料型別的轉換等,而不是使用具體的類

在需要時,將具體類的物件通過依賴注入(DependencyInjection, DI)的方式注入到其他物件中

在引入抽象層後,程式中儘量使用抽象層進行程式設計, 系統將具有很好的靈活性 並且將具體類寫在配置檔案中

如果系統行為發生變化,只需要對抽象層進行擴充套件,並修改配置檔案

而無須修改原有系統的原始碼 , 擴充套件系統的功能無需修改原來的程式碼,滿足開閉原則的要求 

 

介面隔離原則(Interface Segregation Principle,ISP)

使用多個專門的介面,而不使用單一的總介面,即客戶端不應該依賴那些它不需要的介面

根據介面隔離原則,當一個介面太大時,我們需要將它分割成一些更細小的介面,使用該介面的客戶端僅需知道與之相關的方法即可

介面隔離根本在於不要強迫客戶端程式依賴他們不需要使用的方法

 

合成/聚合複用原則(Composite/Aggregate Reuse Principle,C/ARP)

複用一個類有兩種常用形式,繼承和組合

儘量使用物件組合,而不是繼承來達到複用的目的,因為繼承子類可以覆蓋父類的方法,將細節暴露給子類

而且會建立強耦合關係,是一種靜態關係,不能再執行時更改等等弊端

個人建議,對於繼承的態度是不濫用,不棄用,帶著腦子用!

 

迪米特法則(Principle of Least Knowledge,PLK,也叫最小知識原則)

一個軟體實體應當儘可能少地與其他實體發生相互作用

也就是一個物件應當對其他物件有儘可能少的瞭解

再設計系統時,應該儘可能的減少物件之間的互動

有一個形象的說法"不要和“陌生人”說話、只與你的直接朋友通訊"

下面這些一般被認為是朋友

 

(1) 當前物件本身(this); (2) 以引數形式傳入到當前物件方法中的物件; (3) 當前物件的成員物件; (4) 如果當前物件的成員物件是一個集合,那麼集合中的元素也都是朋友; (5) 當前物件所建立的物件 其實也仍舊是"不要和其他物件有過多的聯絡",只在必要的時候與外界進行聯絡

 

"不要和“陌生人”說話、只與你的直接朋友通訊" 就能夠最大程度的降低耦合性

類之間的耦合度越低,就越有利於複用

如果兩個物件之間不是必須要直接通訊,那麼這兩個物件就可以不發生任何直接的相互作用

而是可以通過第三者轉發這個呼叫,通過引入第三者將耦合度降低

 

設計原則總結

 

開閉原則 軟體實體應該對擴充套件開放,對修改關閉。 開閉原則是設計原則的核心原則,其他的設計原則都是開閉原則表現和補充。實現開閉原則的方法就是抽象。 單一職責原則 一個類應該只承擔一種責任。 里氏替換原則 所有引用基類的地方必須能透明地使用其子類的物件。 依賴倒置原則 面向抽象程式設計,不要面向具體程式設計。 介面隔離原則 使用專門的介面,而不是大而全統一的介面,不要強迫客戶端程式依賴不需要的方法。 聚合/組合複用原則 如果可以,應該使用組合而不是繼承來達到程式碼複用的目的。 迪米特法則: 軟體實體之間應該做到最少的互動。不要和陌生人說話。  

設計原則要求

設計原則是指導思想,將規則落實到具體的類/介面的設計、功能邏輯的劃分上,可以轉化成以下要求

所有的要求都有一個前提:如果可以,應該優先考慮,儘可能的

  • 面向抽象(抽象類、介面)程式設計,而不是面向實現程式設計
  • 介面和類的功能要儘可能的單一,避免大而全的類和介面
  • 優先使用組合,而不是繼承
  • 子類的所有方法必須在父類中宣告,或子類必須實現父類中宣告的所有方法
  • 子類應該儘可能的與父類保持一致,不要重寫父類原有邏輯
  • 如果類之間沒必要直接互動,可以通過“中介”,而不是直接互動,降低耦合性
  • 實現和細節可以通過DI的方式,最大程度減少“硬編碼”
  • 如果沒有什麼明顯弊端,類應該被設計成不變的
  • 降低其他類對自身的訪問許可權,不要暴露內部屬性成員,如果需要提供相應的訪問器(屬性) 
 

設計模式與設計原則

設計原則是軟體開發過程中,前人以“高內聚,低耦合” “提高複用性”“提高可維護性”為根本目標

在實踐中總結出來的經驗,進而演化出來的具體的行為準則

 

就好似要做“好”一件事情,那麼“好”的標準是什麼?

按照經驗總結歸納出來的一些“好”的標準,就是程式設計中的設計原則

 

設計原則是站在不同的維度與角度思考問題的, 他們的根本目的是相同的

本質都是為了設計一個“易維護、可複用、高內聚低耦合”的程式

比如單一職責原則與介面隔離原則,本質都是要職責專一

類提供單一的功能的實現,介面不要有大而全的功能約定

職責專一就能降低耦合,就更有可能被複用

使用組合而不是繼承可以避免子類對父類的修改這種情況也就符合了里氏替換原則,也就符合了開閉原則

依賴倒置原則要求面向抽象進行程式設計而不是面向具體細節,而且依賴注入DI的思想也是如日中天Spring的根本

 

“易維護、可複用、高內聚低耦合”是目標

設計原則是為了達到目標的具體規則

而設計模式則是符合設計規則的具體的類/介面的設計解決方案

也就是設計原則的具體化形式

更準確的說,一個設計良好的程式應該遵循的是設計原則,而並非一定是某個設計模式

所有的原則都是指導方針,而不是硬性規則

是在很多場景下一種優秀的解決方案,而並不是一成不變的

在實際的專案中,你既不能完全放棄使用繼承,也可能讓一個類完全不同“陌生人”講話

也不可能子類完全不重寫父類的方法

面向抽象進行程式設計,你也不可能讓專案中所有的類都有抽象對應,這也是不可能的,也不能是被允許的

設計模式設計原則是經驗之談,當然是非常寶貴的經驗,也是經過實踐檢驗過的

但是最大的忌諱就是生搬硬套,矯枉過正,那將是最失敗的設計模式的應用方式

 

設計模式和麵向物件的設計原則是解決問題的一般思路

而不是像交規一樣,必須遵守,嚴格執行

不遵守設計原則與設計模式也不會編譯失敗

但是希望能夠盡最大可能的遵守, 當然,還需要因地制宜而不能生搬硬套

或許,你從來不遵守原則,也不使用設計模式,你的程式碼可能看起來仍舊好好地

但是

你的程式碼出問題的概率

卻會比使用了設計模式遵循了設計原則的程式碼

要大得多

 

設計模式和設計原則正是為了能夠更加簡單便利的複用程式碼,儘可能的減少問題的出現

就好像一條淺淺的小河,可能有無數種趟過去方案

但是,那條走的人最多的,可能它並不是最好的

但是他肯定是比較合適的一條途徑,不會出現碎玻璃,沙坑等陷阱.

到底是站在巨人的肩膀上還是一定要自己摸著石頭過河?

 

簡單說來就是:我們知道軟體的目標“正確、健壯、靈活、可重用、高效....”等等,總之都是往“優秀”“好”的方向

然後發現了好的軟體的一些特性,所以作為了設計原則

但是還是過於抽象,於是針對於不同的場景,按照設計原則,整理出來一套好的解決方法,這就是設計模式。

 

設計模式分類

關於設計模式與設計原則,設計模式是設計原則的具體化形式,是針對於某些特定場景的具體化解決方案 具體到類/介面的設計組織邏輯 既然是原則的具體化形式,那麼必然,按照原則的合理組合運用以及問題的場景,其實可以延伸出來更多的設計模式 在Design Patterns - Elements of Reusable Object-Oriented Software(中文譯名:設計模式 - 可複用面向物件軟體的基礎)中 有23種設計模式,按照特點可以將其分為三大型別:建立型結構型行為型  建立型模式是用來建立物件的模式,抽象了例項的建立過程,封裝了建立邏輯
  • 將系統所使用的具體類的資訊封裝起來
  • 隱藏了類的例項是如何被建立和組織的
結構型模式討論的是類和物件的結構,繼承和組合結構 採用繼承機制來組合介面或實現(類結構型模式),或者通過組合一些物件實現新的功能(物件結構型模式) 結構中的各個不同角色組織在一起以提供更強大的、邏輯清晰的功能 結構性模式的類層次結構的組織上有很大的相似性,但是邏輯的側重功能點是不同的   行為型設計模式關注的是物件的行為,用來解決物件之間的聯絡/通訊問題 也就是物件之間針對於不同的問題場景,如何進行合理互動   建立型 工廠模式(Factory Pattern) 抽象工廠模式(Abstract Factory Pattern) 單例模式(Singleton Pattern) 建造者模式(Builder Pattern) 原型模式(Prototype Pattern)   結構型 介面卡模式(Adapter Pattern) 橋接模式(Bridge Pattern) 組合模式(Composite Pattern) 裝飾器模式(Decorator Pattern) 外觀模式(Facade Pattern) 享元模式(Flyweight Pattern) 代理模式(Proxy Pattern)  

行為型

責任鏈模式(Chain of Responsibility Pattern) 命令模式(Command Pattern) 直譯器模式(Interpreter Pattern) 迭代器模式(Iterator Pattern) 中介者模式(Mediator Pattern) 備忘錄模式(Memento Pattern) 觀察者模式(Observer Pattern) 狀態模式(State Pattern) 策略模式(Strategy Pattern) 模板模式(Template Pattern) 訪問者模式(Visitor Pattern)

 

設計模式之間並不是孤立的,他們也會相互使用,下圖為《設計模式 - 可複用面向物件軟體的基礎》一書中的描述

各個模式之間的區別和聯絡是一個“悟”的過程,不要試圖對下下圖進行任何記憶

 

 

image_5bdc195e_2288

另外還有範圍準則的概念,指定模式主要是用於類還是用於物件

類模式處理類和子類之間的關係,這些關係通過繼承建立,是靜態的,編譯時刻便已經確定下來了

物件模式處理物件之間的關係,這些關係在執行時是可以變化的,更具有動態性

其實如果較真,很多的模式都有涉及到繼承/實現

所以說設計模式中常說的“類模式”只是指那些集中於處理類間關係的模式

大部分模式都屬於物件模式

比如對於建立型來說分為 類建立型模式和物件建立型模式

概念只是為了更好的描述問題,類模式和物件模式的概念也來自《設計模式 - 可複用面向物件軟體的基礎》

本人認為對於設計模式一般的學習與理解,這個概念無所謂

 

總結

設計模式是設計原則在解決具體問題時實踐中的運用

所以根本是要理解設計原則的含義

隨著技術發展,會出現更多的不同的問題場景,基於設計原則,可能拓展出來更多的設計模式

事實上到目前為止,也不僅僅是23種

所以說設計模式的根本是設計原則,而設計原則又是為了達到實現一個“優秀”軟體的行為準則。

在你還不能靈活的運用設計原則時,設計模式則是你的墊腳石,讓你在具體的問題面前能夠寫出更好地程式碼

設計模式是理論層次的研究學習,自然是枯燥的

而且很難能夠一開始就高屋建瓴的自頂而下的深入理解

也很難徹底領悟設計原則本身

所以,從一個一個模式的學習中慢慢品味設計原則的精髓

 

類、介面之間的層級結構是可以變換的,設計模式的根本是設計原則

所以說在學習中要領悟設計模式的根本思想使用場景

在實踐中,不要生搬硬套的應用模式,也無需同設計模式中的類、介面設計層級結構一模一樣

可能你應用了某個模式,但是可能又根據實際業務有一些變動或調整

有人說,你這不是設計模式,那又如何?

只要能夠滿足需求符合設計原則,往“可複用/易維護/高內聚/低耦合”的目標前進,就好~

 

設計模式將“只可意會,不可言傳”轉變為“不只意會,還可以言傳~”