“宇宙萬物之中,沒有一樣東西能像思想那么頑固。” 一愛默生
首先明確模式是針對面向對象的,它的三大特性,封裝、繼承、多態。
面向對象設計模式有5大基本原則:單一職責原則、開發封閉原則、依賴倒置原則、接口隔離原則、Liskov替換原則。
而設計模式都是在面向對象的特性以及5大基本原則的基礎上衍生而來的具體實現。
1、單一職責原則(SRP):
1.1,SRP(Single Responsibilities Principle)的定義:就一個類而言,應該僅有一個引起它變化的原因。簡而言之,就是功能要單一。
1.2,如果一個類承擔的職責過多,就等於把這些職責耦合在一起,一個職責的變化可能會削弱或者抑制這個類完成其它職責的能力。這種耦合會導致脆弱的設計,當變化發生時,設計會遭受到意想不到的破壞。(敏捷軟件開發)
1.3,軟件設計真正要做的許多內容,就是發現職責並把那些職責相互分離。(敏捷軟件開發)
小結:
單一職責原則可以看做是低耦合、高內聚在面向對象原則上的引申,將職責定義為引起變化的原因,以提高內聚性來減少引起變化的原因。職責過多,可能引起它變化的原因就越多,這樣導致職責依賴,相互之間就會產生原因,大大損傷其內聚性和耦合度。
2、開放-封閉原則(OCP):
2.1,OCP(Open-Close Principle)的定義:就是說軟件實體(類,方法等等)應該可以擴展,但是不能修改。它是軟件設計中也是最重要的一種設計原則。
2.2,OCP的兩個特征:
2.2.1> 對於擴展是開放的。
2.2.2> 對於修改是封閉的。
2.3,什么時候應用OCP原則呢?
在我們最初編寫代碼時,假設變化不會發生,當變化發生時,我們就創建抽象(比如抽象類,接口等等)來隔離以后發生的同類變化。
2.4,開放-封閉原則是面向對象設計的核心所在。遵循這個原則可以帶來面向對象技術所聲稱的巨大好處,也就是可維護,可擴展,可復用,靈活性好。開發人員應該僅對程序中呈現出頻繁變化的那些部分做出抽象,然而,對於應用程序中的每個部分都刻意地進行抽象同樣不是一個好主意。拒絕不成熟的抽象和抽象本身一樣重要。
2.5,OCP的UML圖:

小結:
開放封閉原則是面向對象設計的核心所在。遵循這個原則可以帶來面向對象技術所聲稱的巨大好處:可維護、可擴展、可復用、靈活性好。開發人員應該僅對程序中呈現頻繁變化的那些部分做出抽象,然而,對於應用程序中的每個部分都刻意地進行抽象同樣也不是一個好主意。拒絕不成熟的抽象和抽象本身一樣重要。“需求總是變化”沒有不變的軟件,所以需要用開放封閉原則來封閉變化滿足需求,同時還能保持軟件內部的封裝體系穩定,不被需求的變化影響。
3、依賴倒轉原則(DIP):
3.1,DIP(Dependence Inversion Principle)的定義:抽象不應該依賴細節,細節應該依賴於抽象。簡單說就是,我們要針對接口編程,而不要針對實現編程。
3.1. 1 高層模塊不應該依賴低層模塊。兩個都應該依賴抽象。
3.1.2 抽象不應該依賴具體(細節)。具體(細節)應該依賴抽象。
3.2、反面例子UML圖:

缺點:高層模塊太依賴低層模塊,耦合太緊密。低層模塊發生變化會影響到高層模塊。
解決方法:利用依賴倒置原則使高層模塊和低層模塊都依賴於抽象(接口或抽象類)。
3.3、修改后的UML圖如下:

優點:這樣的話修改低層模塊不會影響到高層模塊,減小了它們之間的耦合度,增強系統的穩定性。
小結:
依賴倒置原則其實可以說是面向對象設計的標志,用哪種語言來編寫程序不重要,如果編寫時考慮的都是如何針對抽象編程而不是針對細節編程,即程序中所有的依賴關系都是終止於抽象類或者接口,那就是面向對象的設計,反之那就是過程化的設計了。
4、接口隔離原則:
使用多個專門的接口比使用單一的總接口要好。
一個類對另外一個類的依賴性應當是建立在最小的接口上的。
一個接口代表一個角色,不應當將不同的角色都交給一個接口。沒有關系的接口合並在一起,形成一個臃腫的大接口,這是對角色和接口的污染。
“不應該強迫客戶依賴於它們不用的方法。接口屬於客戶,不屬於它所在的類層次結構。”這個說得很明白了,再通俗點說,不要強迫客戶使用它們不用的方法,如果強迫用戶使用它們不使用的方法,那么這些客戶就會面臨由於這些不使用的方法的改變所帶來的改變。
小結:
接口隔離的方法有兩種(分享客戶就是分離接口):
1、使用委托(此委托非.net委托[delegate])分離接口
使用委托即,創建一個委托類,用此類去實現分離后的其它接口中的方法。
2、使用多重繼承分離接口、
此方法,即將現有“胖”接口分成供不同客戶程序調用的兩個或多個接口,而需要實現多個接口的客戶程序,則使用多重繼承來實現。
5、Liskov(里氏)替換原則(LSP):
5.1,LSP(Liskov Substitution Principle)的定義:子類型必須能夠替換掉它們的父類型。簡單地說,這是因為子類型繼承了父類,所以子類可以以父類的身份出現。
實例UML圖:

C#代碼:
public class Animal { public void Eat() { } public void Drink() { } public void Run() { } public void Shout() { } } public class Cat : Animal { } public class Dog : Animal { } public class Cattle : Animal { } public class Sheep : Animal { } class Program { static void Main(string[] args) { Animal animal = new Cat();//根據需求的變化,這里可以替換成Dog,Cattle或Sheep,程序其它地方不需要改變 animal.Eat(); animal.Drink(); animal.Run(); animal.Shout(); } }
小結:
任何基類可以出現的地方,子類一定可以出現。 LSP是繼承復用的基石,只有當衍生類可以替換掉基類,軟件單位的功能不受到影響時,基類才能真正被復用,而衍生類也能夠在基類的基礎上增加新的行為。里氏代換原則是對“開-閉”原則的補充。實現“開-閉”原則的關鍵步驟就是抽象化。而基類與子類的繼承關系就是抽象化的具體實現,所以里氏代換原則是對實現抽象化的具體步驟的規范。
補充:
迪米特法則(LoD):
自從我們接觸編程開始,就知道了軟件編程的總的原則:低耦合,高內聚。無論是面向過程編程還是面向對象編程,只有使各個模塊之間的耦合盡量的低,才能提高代碼的復用率。低耦合的優點不言而喻,但是怎么樣編程才能做到低耦合呢?那正是迪米特法則要去完成的。
迪米特法則又叫最少知道原則,最早是在1987年由美國Northeastern University的Ian Holland提出。通俗的來講,就是一個類對自己依賴的類知道的越少越好。也就是說,對於被依賴的類來說,無論邏輯多么復雜,都盡量地的將邏輯封裝在類的內部,對外除了提供的public方法,不對外泄漏任何信息。迪米特法則還有一個更簡單的定義:只與直接的朋友通信。首先來解釋一下什么是直接的朋友:每個對象都會與其他對象有耦合關系,只要兩個對象之間有耦合關系,我們就說這兩個對象之間是朋友關系。耦合的方式很多,依賴、關聯、組合、聚合等。其中,我們稱出現成員變量、方法參數、方法返回值中的類為直接的朋友,而出現在局部變量中的類則不是直接的朋友。也就是說,陌生的類最好不要作為局部變量的形式出現在類的內部。
1,LoD(Law of Demeter)的定義:如果兩個類不必彼此直接通信,那么這兩個類就不應當直接的相互作用。如果其中一個類需要調用另一個類的某一個方法的話,可以通過第三者轉發這個調用。
2,在類的結構設計上,每一個類都應當盡量降低成員的訪問權限,也就是說,一個類包裝好自己的private狀態,不需要讓別的類知道的字段或行為(方法)就盡量不要公開。
定義:一個對象應該對其他對象保持最少的了解。
問題由來:類與類之間的關系越密切,耦合度越大,當一個類發生改變時,對另一個類的影響也越大。
解決方案:盡量降低類與類之間的耦合。
舉例:有一個集團公司,下屬單位有分公司和直屬部門,現在要求打印出所有下屬單位的員工ID。先來看一下違反迪米特法則的設計。
C#代碼如下:
//總公司員工 class Employee{ private String id; public void setId(String id){ this.id = id; } public String getId(){ return id; } } //分公司員工 class SubEmployee{ private String id; public void setId(String id){ this.id = id; } public String getId(){ return id; } } class SubCompanyManager{ public List<SubEmployee> getAllEmployee(){ List<SubEmployee> list = new ArrayList<SubEmployee>(); for(int i=0; i<100; i++){ SubEmployee emp = new SubEmployee(); //為分公司人員按順序分配一個ID emp.setId("分公司"+i); list.add(emp); } return list; } } class CompanyManager{ public List<Employee> getAllEmployee(){ List<Employee> list = new ArrayList<Employee>(); for(int i=0; i<30; i++){ Employee emp = new Employee(); //為總公司人員按順序分配一個ID emp.setId("總公司"+i); list.add(emp); } return list; } public void printAllEmployee(SubCompanyManager sub){ List<SubEmployee> list1 = sub.getAllEmployee(); for(SubEmployee e:list1){ System.out.println(e.getId()); } List<Employee> list2 = this.getAllEmployee(); for(Employee e:list2){ System.out.println(e.getId()); } } } public class Client{ public static void main(String[] args){ CompanyManager e = new CompanyManager(); e.printAllEmployee(new SubCompanyManager()); } }
現在這個設計的主要問題出在CompanyManager中,根據迪米特法則,只與直接的朋友發生通信,而SubEmployee類並不是CompanyManager類的直接朋友(以局部變量出現的耦合不屬於直接朋友),從邏輯上講總公司只與他的分公司耦合就行了,與分公司的員工並沒有任何聯系,這樣設計顯然是增加了不必要的耦合。按照迪米特法則,應該避免類中出現這樣非直接朋友關系的耦合。
修改后的C#代碼如下:
class SubCompanyManager{ public List<SubEmployee> getAllEmployee(){ List<SubEmployee> list = new ArrayList<SubEmployee>(); for(int i=0; i<100; i++){ SubEmployee emp = new SubEmployee(); //為分公司人員按順序分配一個ID emp.setId("分公司"+i); list.add(emp); } return list; } public void printEmployee(){ List<SubEmployee> list = this.getAllEmployee(); for(SubEmployee e:list){ System.out.println(e.getId()); } } } class CompanyManager{ public List<Employee> getAllEmployee(){ List<Employee> list = new ArrayList<Employee>(); for(int i=0; i<30; i++){ Employee emp = new Employee(); //為總公司人員按順序分配一個ID emp.setId("總公司"+i); list.add(emp); } return list; } public void printAllEmployee(SubCompanyManager sub){ sub.printEmployee(); List<Employee> list2 = this.getAllEmployee(); for(Employee e:list2){ System.out.println(e.getId()); } } }
修改后,為分公司增加了打印人員ID的方法,總公司直接調用來打印,從而避免了與分公司的員工發生耦合。
小結:
迪米特法則的初衷是降低類之間的耦合,由於每個類都減少了不必要的依賴,因此的確可以降低耦合關系。但是凡事都有度,雖然可以避免與非直接的類通信,但是要通信,必然會通過一個“中介”來發生聯系,例如本例中,總公司就是通過分公司這個“中介”來與分公司的員工發生聯系的。過分的使用迪米特原則,會產生大量這樣的中介和傳遞類,導致系統復雜度變大。所以在采用迪米特法則時要反復權衡,既做到結構清晰,又要高內聚低耦合。
