【設計模式】設計模式6大原則
原貼:http://www.manew.com/thread-22531-1-1.html
單一職責原則
例如:
class Animal { public void breathe(string animal) { Debug.Log(animal+"呼吸空氣"); } } public class Client { Animal animal = new Animal(); void Start() { animal.breathe("牛"); animal.breathe("羊"); animal.breathe("豬"); } }
執行結果: 牛呼吸空氣 ,羊呼吸空氣,豬呼吸空氣
後新增新需求-比如魚就是呼吸水的。
修改時如果遵循單一職責原則,需要將Animal類細分為陸生動物類Terrestrial,水生動物Aquatic,程式碼如下:
class Terrestrial { public void breathe(String animal){ Debug.Log(animal + "呼吸空氣"); } } class Aquatic { public void breathe(String animal){ Debug.Log(animal + "呼吸水"); } } public class Client { public static void main(String[] args) { Terrestrial terrestrial = new Terrestrial(); Debug.Log(terrestrial.breathe("牛")); Debug.Log(terrestrial.breathe("羊")); Debug.Log(terrestrial.breathe("豬")); Aquatic aquatic = new Aquatic(); Debug.Log( aquatic.breathe("魚")); } }
執行結果: 牛呼吸空氣,羊呼吸空氣,豬呼吸空氣,魚呼吸水
我們會發現如果這樣修改花銷是很大的,除了將原來的類分解之外,還需要修改客戶端。
而直接修改類Animal來達成目的雖然違背了單一職責原則,但花銷卻小的多(原來方法基礎上加判斷),程式碼如下:
class Animal
{
public void breathe(String animal)
{
if("魚".equals(animal))
{
Debug.Log((animal+"呼吸水"));
}
else
{
Debug.Log((animal+"呼吸空氣"));
}
}
}
public class Client
{
public static void main(String[] args)
{
Animal animal = new Animal();
Debug.Log(animal.breathe("牛"));
Debug.Log(animal.breathe("羊"));
Debug.Log(animal.breathe("豬"));
Debug.Log(animal.breathe("魚"));
}
}
可以看到,這種修改方式要簡單的多。
但是卻存在著隱患:有一天需要將魚分為呼吸淡水的魚和呼吸海水的魚,
則又需要修改Animal類的breathe方法,而對原有程式碼的修改會對呼叫“豬”“牛”“羊”等相關功能帶來風險,
也許某一天你會發現程式執行的結果變為“牛呼吸水”了。
這種修改方式(直接在原來基礎上新增判斷)直接在程式碼級別上違背了單一職責原則,雖然修改起來最簡單,但隱患卻是最大的。
還有一種修改方式:
class Animal
{
public void breathe(String animal)
{
Debug.Log(animal+"呼吸空氣");
}
public void breathe2(String animal)
{
Debug.Log(animal+"呼吸水");
}
}
public class Client
{
public static void main(String[] args)
{
Animal animal = new Animal();
Debug.Log(animal.breathe("牛"));
Debug.Log(animal.breathe("羊"));
Debug.Log(animal.breathe("豬"));
Debug.Log(animal.breathe2("魚"));
}
}
可以看到,這種修改方式沒有改動原來的方法,而是在類中新加了一個方法,這樣雖然也違背了單一職責原則,
但在方法級別上卻是符合單一職責原則的,因為它並沒有動原來方法的程式碼。這三種方式各有優缺點,
那麼在實際程式設計中,採用哪一種呢?
其實這真的比較難說,需要根據實際情況來確定。
我的原則是:只有邏輯足夠簡單,才可以在程式碼級別上遵守單一職責原則;只有類中方法數量足夠少,才可以在方法級別上遵守單一職責原則。
遵循單一職責原的優點有:
- 可以降低類的複雜度,一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單的多;
- 提高類的可讀性,提高系統的可維護性;
- 變更引起的風險降低,變更是必然的,如果單一職責原則遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響。
需要說明的一點是單一職責原則不只是面向物件程式設計思想所特有的,只要是模組化的程式設計,都適用單一職責原則。
里氏替換原則
里氏替換原則通俗的來講就是:子類可以擴充套件父類的功能,但不能改變父類原有的功能。它包含以下4層含義:
- 子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
- 子類中可以增加自己特有的方法。
- 當子類的方法過載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入引數更寬鬆。
- 當子類的方法實現父類的抽象方法時,方法的後置條件(即方法的返回值)要比父類更嚴格
例如:
class A
{
public int func1(int a, int b)
{
return a - b;
}
}
public class Client
{
void Start()
{
A a = new A();
Debug.Log(("100-50="+a.func1(100, 50));
Debug.Log(("100-80="+a.func1(100, 80)));
}
}
執行結果:50,20
現在需要增加一個新的功能,完成兩次相加,繼承自A類的B類來負責。
class B:A{
public int func1(int a, int b)
{
return a+b;
}
public int func2(int a, int b)
{
return func1(a,b)+100;
}
}
public class Client
{
void Start()
{
B b = new B();
Debug.Log("100-50="+b.func1(100, 50));
Debug.Log("100-80="+b.func1(100, 80));
Debug.Log("100+20+100="+b.func2(100, 20));
}
}
執行結果:100-50=150,100-80=180,100+20+100=220
B類重寫A類方法func1後,導致A類的相減方法出錯。
如果非要重寫父類的方法,比較通用的做法是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關係去掉,採用依賴、聚合,組合等關係代替。
依賴倒置原則
定義:高層模組不應該依賴低層模組,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。
以抽象為基礎搭建起來的架構比以細節為基礎搭建起來的架構要穩定的多。
抽象指的是介面或者抽象類,細節就是具體的實現類,使用介面或者抽象類的目的是制定好規範和契約,而不去涉及任何具體的操作,把展現細節的任務交給他們的實現類去完成。依賴倒置原則的核心思想是面向介面程式設計
例如:
母親給孩子講故事,只要給她一本書,她就可以照著書給孩子講故事了。程式碼如下:
class Book{
public String getContent()
{
return "很久很久以前有一個阿拉伯的故事……";
}
}
class Mother{
public void narrate(Book book)
{
Debug.Log("媽媽開始講故事");
Debug.Log(book.getContent());
}
}
public class Client
{
void Start()
{
Mother mother = new Mother();
Debug.Log(mother.narrate(new Book()));
}
}
執行結果:
媽媽開始講故事
很久很久以前有一個阿拉伯的故事……
執行良好,假如有一天,需求變成這樣:不是給書而是給一份報紙,讓這位母親講一下報紙上的故事,報紙的程式碼如下:
class Newspaper
{
public String getContent()
{
return "林書豪38+7領導尼克斯擊敗湖人……";
}
}
這位母親卻辦不到,因為她居然不會讀報紙上的故事,這太荒唐了,只是將書換成報紙,居然必須要修改Mother才能讀。
假如以後需求換成雜誌呢?換成網頁呢?
還要不斷地修改Mother,這顯然不是好的設計。
原因就是Mother與Book之間的耦合性太高了,必須降低他們之間的耦合度才行。
我們引入一個抽象的介面IReader。
讀物,只要是帶字的都屬於讀物:
interface IReader
{
public String getContent();
}
Mother類與介面IReader發生依賴關係,而Book和Newspaper都屬於讀物的範疇,
他們各自都去實現IReader介面,這樣就符合依賴倒置原則了,程式碼修改為:
class Newspaper : IReader
{
public String getContent()
{
return "林書豪17+9助尼克斯擊敗老鷹……";
}
}
class Book : IReader
{
public String getContent()
{
return "很久很久以前有一個阿拉伯的故事……";
}
}
class Mother
{
public void narrate(IReader reader)
{
Debug.Log("媽媽開始講故事");
Debug.Log(reader.getContent());
}
}
public class Client
{
public static void main(String[] args)
{
Mother mother = new Mother();
Debug.Log(mother.narrate(new Book()));
Debug.Log(mother.narrate(new Newspaper()));
}
}
採用依賴倒置原則給多人並行開發帶來了極大的便利,
比如上例中,原本Mother類與Book類直接耦合時,Mother類必須等Book類編碼完成後才可以進行編碼,因為Mother類依賴於Book類。
修改後的程式則可以同時開工,互不影響,因為Mother與Book類一點關係也沒有。
參與協作開發的人越多、專案越龐大,採用依賴導致原則的意義就越重大。
現在很流行的TDD開發模式就是依賴倒置原則最成功的應用。
在實際程式設計中,我們一般需要做到如下3點:
- 低層模組儘量都要有抽象類或介面,或者兩者都有。
- 變數的宣告型別儘量是抽象類或介面。使用繼承時遵循里氏替換原則。
- 依賴倒置原則的核心就是要我們面向介面程式設計,理解了面向介面程式設計,也就理解了依賴倒置。
介面隔離原則
客戶端不應該依賴它不需要的介面;一個類對另一個類的依賴應該建立在最小的介面上。
將臃腫的介面I拆分為獨立的幾個介面,類A和類C分別與他們需要的介面建立依賴關係。也就是採用介面隔離原則。
舉例來說明介面隔離原則
介面隔離原則的含義是:
建立單一介面,不要建立龐大臃腫的介面,儘量細化介面,介面中的方法儘量少。
也就是說,我們要為各個類建立專用的介面,而不要試圖去建立一個很龐大的介面供所有依賴它的類去呼叫。
在程式設計中,依賴幾個專用的介面要比依賴一個綜合的介面更靈活。
介面是設計時對外部設定的“契約”,通過分散定義多個介面,可以預防外來變更的擴散,提高系統的靈活性和可維護性
採用介面隔離原則對介面進行約束時,要注意以下幾點:
- 介面儘量小,但是要有限度。對介面進行細化可以提高程式設計靈活性是不爭的事實,但是如果過小,則會造成介面數量過多,使設計複雜化。所以一定要適度。
- 為依賴介面的類定製服務,只暴露給呼叫的類它需要的方法,它不需要的方法則隱藏起來。只有專注地為一個模組提供定製服務,才能建立最小的依賴關係。
- 提高內聚,減少對外互動。使介面用最少的方法去完成最多的事情。
例如:
interface I {
public void method1();
public void method2();
public void method3();
public void method4();
public void method5();
}
class A : I{
public void method1()
{
Debug.Log("類A實現介面I的方法1");
}
public void method2()
{
Debug.Log("類A實現介面I的方法2");
}
public void method3()
{
Debug.Log("類A實現介面I的方法3");
}
//對於類A來說,method4和method5不是必需的,但是由於介面A中有這兩個方法,
//所以在實現過程中即使這兩個方法的方法體為空,也要將這兩個沒有作用的方法進行實現。
public void method4() {}
public void method5() {}
}
class B : I{
//對於類B來說,method1和method2不是必需的,但是由於介面中有 //這兩個方法,
//所以在實現過程中即使這兩個方法的方法體為空,也要將這兩個沒有作 //用的方法進行實現。
public void method1()
{
}
public void method2()
{
}
public void method3()
{
Debug.Log("類B實現介面I的方法3");
}
public void method4()
{
Debug.Log("類B實現介面I的方法4”);
}
public void method5()
{
Debug.Log("類B實現介面I的方法5”);
}
}
依照介面隔離原則:
interface I {
public void method1();
public void method2();
public void method3();
public void method4();
public void method5();
}
分為:
interface K {
public void method1();
public void method2();
}
interface P {
public void method3();
public void method4();
public void method5();
}
Class A,Class B分別實現介面K和P
介面隔離原則和單一職責原則的區別:
- 單一職責原則注重的是職責;而介面隔離原則注重對介面依賴的隔離。
- 單一職責原則主要是約束類,其次才是介面和方法,它針對的是程式中的實現和細節;
- 介面隔離原則主要約束介面,主要針對抽象,針對程式整體框架的構建。
迪米特法則
通俗的來講,就是一個類對自己依賴的類知道的越少越好。也就是說,對於被依賴的類來說,無論邏輯多麼複雜,都儘量地的將邏輯封裝在類的內部,對外除了提供的public方法,不對外洩漏任何資訊。
每個物件都會與其他物件有耦合關係,只要兩個物件之間有耦合關係,我們就說這兩個物件之間是朋友關係。
耦合的方式很多,依賴、關聯、組合、聚合等。其中,我們稱出現成員變數、方法引數、方法返回值中的類為直接的朋友。
有一個集團公司,下屬單位有分公司和直屬部門,現在要求打印出所有下屬單位的員工ID
//總公司員工
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 List<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 List<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();
foreach (SubEmployee e in list1)
{
Debug.Log(e.getId());
}
List<Employee> list2 = this.getAllEmployee();
foreach (Employee e in list2)
{
Debug.Log(e.getId());
}
}
}
public class Client
{
void Start()
{
CompanyManager e = new CompanyManager();
Debug.Log(e.printAllEmployee(new SubCompanyManager()));
}
}
而出現在區域性變數中的類則不是直接的朋友。也就是說,陌生的類最好不要作為區域性變數的形式出現在類的內部。
現在這個設計的主要問題出在CompanyManager中,根據迪米特法則,只與直接的朋友發生通訊,
而SubEmployee類並不是CompanyManager類的直接朋友(以區域性變量出現的耦合不屬於直接朋友),從邏輯上講總公司只與他的分公司耦合就行了。
修改為:
class SubCompanyManager
{
public List<SubEmployee> getAllEmployee()
{
List<SubEmployee> list = new List<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();
foreach (SubEmployee e in list)
{
Debug.Log(e.getId());
}
}
}
class CompanyManager
{
public List<Employee> getAllEmployee()
{
List<Employee> list = new List<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();
foreach (Employee e in list2)
{
Debug.Log(e.getId());
}
}
}
過分的使用迪米特原則,會產生大量這樣的中介和傳遞類,導致系統複雜度變大。
所以在採用迪米特法則時要反覆權衡,既做到結構清晰,又要高內聚低耦合。
開閉原則(對擴充套件開放,對修改關閉)
可在原來框架下擴充套件實現新功能,而不對原來原有的類和方法進行修改。
用抽象構建框架,用實現擴充套件細節。
因為抽象靈活性好,適應性廣,只要抽象的合理,可以基本保持軟體架構的穩定。
而軟體中易變的細節,我們用從抽象派生的實現類來進行擴充套件,當軟體需要發生變化時,我們只需要根據需求重新派生一個實現類來擴充套件就可以了。