1.設計模式的目的
設計模式是為了更好的代碼重用性,可讀性,可靠性,可維護性。
2.常用的六大設計模式
1)單一職責原則
2)里氏替換原則
3)依賴倒轉原則
4)接口隔離原則
5)迪米特法則
6)開閉原則
3.單一職責原則
該原則是針對類來說的,即一個類應該只負責一項職責。
如類T負責兩個不同職責:職責P1,職責P2。當職責P1需求變更而改變T時,可能造成職責P2發生故障,所以需要將類T的粒度分解為T1,T2。
示例如下:
用一個類秒數動物呼吸這個場景
class Animal { public void breathe(string animal) { Console.WriteLine(animal+"呼吸空氣"); } } class Program { static void Main(string[] args) { Animal animal = new Animal(); animal.breathe("牛"); animal.breathe("羊"); animal.breathe("豬"); animal.breathe("魚"); Console.ReadLine(); } }
輸出結果:
我們發現不是所有動物都是呼吸空氣的,比如魚就是呼吸水的,根據單一職責原則,我們將Animal類細分為陸生動物類和水生動物類,如下所示:
class Terrestrial { public void breathe(string animal) { Console.WriteLine(animal+"呼吸空氣"); } } class Aquatic { public void breathe(string animal) { Console.WriteLine(animal + "呼吸水"); } } class Program { static void Main(string[] args) { Terrestrial terrestrial = new Terrestrial(); terrestrial.breathe("牛"); terrestrial.breathe("羊"); terrestrial.breathe("豬"); Aquatic aquatic = new Aquatic(); aquatic.breathe("魚"); Console.ReadLine(); } }
我們發現這樣修改的花銷很大,既要將原來的類分解,又要修改客戶端。而直接修改Animal類雖然違背了單一職責原則,但花銷小的多,如下所示:
class Animal { public void breathe(string animal) { if ("魚".Equals(animal)) { Console.WriteLine(animal + "呼吸水"); } else { Console.WriteLine(animal + "呼吸空氣"); } } } class Program { static void Main(string[] args) { Animal animal = new Animal(); animal.breathe("牛"); animal.breathe("羊"); animal.breathe("豬"); animal.breathe("魚"); Console.ReadLine(); } }
可以看到,這種修改方式簡單的多。但卻存在隱患,一天需要將魚分為淡水魚,海水魚,又需要修改Animal類的breathe方法。可能給“豬牛羊”等相關功能帶來風險,這種修改直接在代碼級別違背了單一職責原則,雖然修改起來最簡單,但隱患最大。還有一種修改方式:
class Animal { public void breathe(string animal) { Console.WriteLine(animal + "呼吸空氣"); } public void breathe2(string animal) { Console.WriteLine(animal + "呼吸水"); } } class Program { static void Main(string[] args) { Animal animal = new Animal(); animal.breathe("牛"); animal.breathe("羊"); animal.breathe("豬"); animal.breathe2("魚"); Console.ReadLine(); } }
這種修改方式沒有改動原來的方法,而是在類中新加了一個方法,這樣雖然違背了單一職責原則,但在方法級別上卻是符合單一職責原則的。那么在實際編程中,采用哪一種呢?我的原則是,只有邏輯足夠簡單,才可以在代碼級違反單一職責原則;只有類中方法數量足夠少,才可以在方法級別違反單一職責原則。
遵循單一職責的優點:
1)降低類的復雜度,一個類只負責一項職責。
2)提高類的可讀性,可維護性
3)降低變更引起的風險。
4.里氏替換原則
該原則是在1988年,由麻省理工學院的以為姓里的女士提出的。
如果對每個類型為T1的對象o1,都有類型為T2的對象o2,使得以T1定義的所有程序P在所有的對象o1都代換成o2時,程序P的行為沒有發生變化,那么類型T2是類型T1的子類型。
換句話說,所有引用基類的地方必須能透明地使用其子類的對象。
由定義可知,在使用繼承時,遵循里氏替換原則,在子類中盡量不要重寫和重載父類的方法。
繼承包含這樣一層含義:父類中凡是已經實現好的方法(相對抽象方法而言),實際上是在設定一系列的規范和契約,雖然它不強制要求所有的子類必須遵循這些契約,但是如果子類對這些非抽象方法任意修改,就會對整個繼承體系造成破壞。而里氏替換原則就是表達了這一層含義。
繼承作為面向對象三大特性之一,在給程序設計帶來巨大遍歷的同時,也帶來了弊端。比如使用繼承會給程序帶來侵入性,程序的可移植性降低,增加對象間的耦合性,如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,並且父類修改后,所有涉及到子類的功能都有可能產生故障。
舉例說明繼承的風險,我們需要完成一個兩數相減的功能,由類A來負責。
class A{ public int func1(int a,int b){ return a-b; } } public class Client{ public static void main(string[] args){ A a=new A(); System.out.println("100-50="+a.func1(100,50)); System.out.println("100-80="+a.func1(100,80)); } }
運行結果:
100-50=50
100-80=20
后來,我們需要增加一個新的功能:完成兩數相加,然后再與100求和,由類B來負責。
Class B extends 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{ public static void main(string[] args){ B a=new B(); System.out.println("100-50="+b.func1(100,50)); System.out.println("100-80="+b.func1(100,80)); System.out.println("100+20+100="+b.func2(100,20)); } }
運行結果:
100-50=150
100-80=180
100+20+100=220
我們發現原來運行正常的相減功能發生了錯誤。原因就是類B無意中重寫了父類的方法,造成原有功能出現錯誤。在實際編程中,我們常常會通過重寫父類的方法完成新的功能,這樣寫起來雖然簡單,但整個繼承體系的復用性會比較差。特別是運行多態比較頻繁的時候,如果非要重寫父類的方法,通用的做法是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關系去掉,采用依賴,聚合,組合等關系代替。
5.依賴倒轉原則
高層模塊不應該依賴低層模塊,二者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴抽象。
類A直接依賴類B,如果要將類A改為依賴類C,則必須通過修改類A的代碼來達成。此時,類A一般是高層模塊,負責復雜的業務邏輯,類B和類C是低層模塊,負責基本的原子操作;修改A會給程序帶來風險。
將類A修改未依賴接口I,類B和類C各自實現接口I,類A通過接口I間接與類B或類C發生聯系,則會大大降低修改類A的記幾率。
依賴倒置原則基於這樣一個事實:相對於細節的多變性,抽象的東西要穩定的多。以抽象為基礎搭建的架構比以細節為基礎的架構要穩定的多。在java中,抽象指的是接口或抽象類,細節就是具體的實現類,使用接口或抽象類的目的是制定好規范,而不涉及任何具體的操作,把展現細節的任務交給他們的實現類去完成。
依賴倒置的中心思想是面向接口編程。
代碼示例如下:
class Book { public string getContent() { return "很久很久以前。。。。。"; } } class Mother { public void narrate(Book book) { Console.WriteLine(book.getContent()); } } class Program { static void Main(string[] args) { Mother monther = new Mother(); monther.narrate(new Book()); Console.ReadLine(); } }
運行結果:
如果讀的對象是報紙,雜志,卻發現客戶端不適用了。
我們引入一個抽象的接口IReader,代表讀物
interface IReader{ public string getContent(); }
這樣Mother類與接口IReader發生依賴關系,而Book和Newspaper都屬於讀物的范疇,他們各自都去實現IReader接口,這樣就符合依賴倒置原則了,修改代碼如下:
interface IReader { string getContent(); } class Newspaper: IReader { public string getContent() { return "切爾西豪取12連勝"; } } class Book:IReader { public string getContent() { return "很久很久以前。。。。"; } } class Mother { public void narrate(IReader reader) { Console.WriteLine(reader.getContent()); } } class Program { static void Main(string[] args) { Mother monther = new Mother(); monther.narrate(new Book()); monther.narrate(new Newspaper()); Console.ReadLine(); } }
運行結果:
采用依賴倒置原則給多人並行開發帶來極大的便利,比如上列中Mother類與Book類直接耦合,Mother必須等Book類編碼完成后才可以進行編碼,因為Mother類依賴於Book類。修改后的程序可以同時開工,互不影響。
依賴關系的傳遞有三種方式,接口傳遞,構造方法傳遞和setter方法傳遞。
接口傳遞:
interface IDriver{ public void drive(ICar car); } public class Driver:IDriver{ public void drive(ICar car){ car.run(); } }
構造方法傳遞:
interface IDriver{ public void drive(); } public class Driver implements IDriver{ public ICar car; public Driver(ICar _car){ this.car=_car; } public void drive(){ this.car.run(); } }
setter方式傳遞:
interface IDriver{ public void setCar(ICar car); public void drive(); } public class Driver:IDriver{ PRIVATE ICar car; public void setCar(ICar car){ this.car=car; } public void drive(){ this.car.run(); } }
在實際編程中,一般需要做到如下3點:
低層模塊盡量都要有抽象類或接口,或者兩者都有。
變量的聲明類型盡量是抽象類或接口。
使用繼承時遵循里氏替換原則
6.接口隔離原則
客戶端不應該依賴它不需要的接口;一個類對另一個類的依賴應該建立在最小的接口上。
類A通過接口I依賴類B,類C通過接口I依賴類D,如果接口I對於類A和類C來說不是最小接口,則類B和類D必須去實現他們不需要的方法。
將臃腫的接口I拆分為獨立的幾個接口,類A和類C分別與他們需要的接口建立依賴關系。也就是采用接口隔離原則。
舉例說明接口隔離原則:
這個圖的意思是:類A依賴接口I中的方法1,方法2,方法3,類B是對類A依賴的實現;類C依賴接口I中的方法1,方法4,方法5,類D是對類C依賴的實現。對於類B和類D來說,雖然存在用不到的方法(紅色標記所示),但由於實現了接口I,所以也必須要實現這些用不到的方法。代碼如下:
interface I{ void method1(); void method2(); void method3(); void method4(); void method5(); } class A{ public void depend1(I i){ i.method1(); } public void depend2(I i){ i.method2(); } public void depend3(I i){ i.method3(); } } class C{ public void depend1(I i){ i.method1(); } public void depend2(I i){ i.method4(); } public void depend3(I i){ i.method5(); } } class B:I{ public void method1(){ Console.WriteLine("類B實現接口I的方法1"); } public void method2(){ Console.WriteLine("類B實現接口I的方法2"); } public void method3(){ Console.WriteLine("類B實現接口I的方法3"); } public void method4(){} public void method5(){} } class D:I{ public void method1(){ Console.WriteLine("類B實現接口I的方法1"); } public void method2(){} public void method3(){} public void method4(){ Console.WriteLine("類B實現接口I的方法4"); } public void method5(){ Console.WriteLine("類B實現接口I的方法5"); } } class Program { static void Main(string[] args) { A a=new A(); a.depend1(new B()); a.depend2(new B()); a.depend3(new B()); C c=new C(); c.depend1(new D()); c.depend2(new D()); c.depend3(new D()); Console.ReadLine(); } }
可以看到,接口中出現的方法,不管對依賴於它的類有沒有作用,實現類中都必須去實現這些方法。於是我們將原接口I拆分為三個接口:
代碼如下所示:
interface I1{ void method1(); } interface I2{ void method2(); void method3(); } interface I3{ void method4(); void method5(); } class A{ public void depend1(I1 i){ i.method1(); } public void depend2(I2 i){ i.method2(); } public void depend3(I2 i){ i.method3(); } } class C{ public void depend1(I1 i){ i.method1(); } public void depend2(I3 i){ i.method4(); } public void depend3(I3 i){ i.method5(); } } class B:I1,I2{ public void method1(){ Console.WriteLine("類B實現接口I1的方法1"); } public void method2(){ Console.WriteLine("類B實現接口I2的方法2"); } public void method3(){ Console.WriteLine("類B實現接口I2的方法3"); } } class D:I1,I3{ public void method1(){ Console.WriteLine("類B實現接口I的方法1"); } public void method4(){ Console.WriteLine("類B實現接口I的方法4"); } public void method5(){ Console.WriteLine("類B實現接口I的方法5"); } } class Program { static void Main(string[] args) { A a=new A(); a.depend1(new B()); a.depend2(new B()); a.depend3(new B()); C c=new C(); c.depend1(new D()); c.depend2(new D()); c.depend3(new D()); Console.ReadLine(); } }
說到這里,可能會覺得接口隔離原則和之前的單一職責原則很相似,其實不然。一,單一職責注重職責,而接口隔離原則注重對接口依賴的隔離;二,單一職責是約束類,其次是方法,針對的是程序中的實現和細節;而接口隔離原則約束的是接口,針對的是抽象,程序整體框架的構建。
7.迪米特法則
一個對象應該對其他對象保持最少的了解。
類與類關系越密切,耦合度越大。
迪米特法則又叫最少知道原則,即一個類對自己依賴的類知道的越少越好。也就是說,對於被依賴的類不管多么復雜,都盡量將邏輯封裝在類的內部。對外除了提供的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 ArrayList(SubEmployee); for(int i=0;i<100;i++){ SubEmployee emp=new SubEmployee(); 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(); emp.setId("總公司"+i); list.add(emp); } return list; } publi void printAllEmployee(SubCompanyManager sub){ List<SubEmployee> list1=sub.getAllEmployee(); foreach(SubEmployee e in list1){ Console.WriteLine(e.getId()); } List<Employee> list2=this.getAllEmployee(); foreach(Employee e in list2){ Console.WriteLine(e.getId()); } } } class Program { static void Main(string[] args) { CompanyManager e=new CompanyManager(); e.printAllEmployee(new SubCompanyManager()); Console.ReadLine(); } }
這個設計的問題在於CompanyManager中,SubEmployee類並不是CompanyManager類的直接朋友,按照迪米特法則,應該避免類中出現這樣非直接朋友關系的耦合。修改后的代碼如下:
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()); } } }
迪米特法則的初衷是降低類之間的耦合,由於每個類都減少了不必要的依賴,因此的確可以降低耦合關系。
8.開閉原則
一個軟件實體如類,模塊和函數應該對擴展開放,對修改關閉。用抽象構建框架,用實現擴展細節。
當軟件需要變化時,盡量通過擴展軟件實體的行為來實現變化,而不是通過修改已有的代碼來實現變化。
當我們遵循前面介紹的5大原則,以及使用23中設計模式的目的就是遵循開閉原則。