DesignPattern Principle
關於設計模式原則,有的按照solid原則總結,有的說六原則,大家都總結的都不一致。
運行了一下後,發現不對哈。魚等水生的動物是不能行走的,那麼怎麼辦呢?所以我們要修改一下,遵循單一職責原則,讓陸生動物使用Terrestrial 這個類;而水生動物使用Aquatic 這個類。
今天這裡把我理解的設計模式幾大原則給大家分享一下:
- 單一職責原則
- 開閉原則
- 接口分離原則
- 里氏代換原則
- 依賴倒置原則
- 迪米特原則
- 優先使用組合,而不是使用繼承
單一職責原則
定義:不要存在多於一個導致類變更的原因。通俗的說,即單一職責是說一個類或者一個方法,只做一件事,或者完成一個任務。
問題由來:類T負責兩個不同的職責:職責Z1,職責Z2。當由於職責Z1需求發生改變而需要修改類T時,有可能會導致原本運行正常的職責Z2功能發生故障。
解決:遵循單一職責原則。分別建立兩個類T1、T2,使T1完成職責Z1功能,T2完成職責Z2功能。這樣,當修改類T1時,不會使職責Z2發生故障風險;同理,當修改T2時,也不會使職責Z1發生故障風險。
舉例說明: 用一個類描述動物行走的public class Animal { public void run ( String str ) { System . out . println( "行走" + str); } } public class Client { public static void main ( String [] args ) { Animal animal = new Animal (); animal . run( "牛" ); animal . run( "羊" ); } }
class Terrestrial { public void run ( String animal ){ System . out . println(animal + "陸地行走" ); } } class Aquatic { public void run ( String animal ){ System . out . println(animal + "水中游行" ); } } public class Client { public static void main ( String [] args ) { Terrestrial terrestrial = new Terrestrial (); terrestrial . breathe( "牛" ); terrestrial . breathe( "羊" ); terrestrial . breathe( "豬" ); Aquatic aquatic = new Aquatic (); aquatic . breathe( "魚" ); } }
遵循單一職責原的優點有:
- 可以降低類的複雜度,一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單的多;
- 提高類的可讀性,提高系統的可維護性;
- 變更引起的風險降低,變更是必然的,如果單一職責原則遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響。
- 需要說明的一點是單一職責原則不只是面向對象編程思想所特有的,只要是模塊化的程序設計,都適用單一職責原則。
開閉原則
定義:是說軟件實體(類、模塊、函數等等)應該可以擴展,但是不可修改。開閉原則的核心是:對擴展開放,對修改關閉。
“可變性的封裝原則”意味著兩點:
- 一種可變性不應當散落在代碼的很多角落裡,而應當被封裝到一個對象裡面。繼承應當被看做是封裝變化的方法,而不應當被認為是從一般的對像生成特殊的對象方法。
- 一種可變性不應當與另一種可變性混合在一起。所有的類圖的繼承結構一般不會超過兩層,不然就意味著將兩種不同的可變性混合在一起。
public interface IBoy { //年齡 public int getAge (); //姓名 public String getName (); //長相 public String getFace (); } public class StrongerBoy implements IBoy { private String name; private int age; private String face; public StrongerBoy ( String name , int age , String face , String figure ) { this . name = name; this . age = age; this . face = face; } @Override public int getAge () { return age; } @Override public String getFace () { return face; } @Override public String getName () { return name; } } public class Mans { private final static ArrayList< IBoy > boys = new ArrayList< IBoy > (); //靜態初始化塊 static { boy . add( new StrongerBoy ( "謝霆鋒" , 30 , "帥氣" )); boy . add( new StrongerBoy ( "馮小剛" , 60 , "成熟" )); } public static void main ( String args []) { System . out . println( " ----------美女在這裡---------- " ); for ( IBoy boy : boys ) { System . out . println( "姓名: " + boy . getName() + "年齡: " + boy . getAge() + " 長相: " + boy . getFace()); } } } }
這個程序寫的不錯哈,我運行了一下,感覺不錯。此時問題來了,如果要加個外國名人怎麼辦?修改Iboy 這個接口嗎,這樣做就符合不了開閉原則了。所以,我這裡想到了擴展,但是如何擴展呢?可以定義一個IForeigner 接口繼承自IBoy,在IForeigner 接口中添加國籍屬性getCountry(),然後實現這個接口即可,然後就只需要在場景類中做稍微修改就可以了。
public interface IForeigner extends IBoy { //國籍 public String getCountry (); } public class ForeignerBoy implements IForeigner { private String name; private int age; private String country; private String face; private String figure; public ForeignerBoy ( String name , int age , String country , String face , String figure ) { this . name = name; this . age = age; this . country = country; this . face = face; this . figure = figure; } @Override public String getCountry () { // TODO Auto-generated method stub return country; } @Override public int getAge () { // TODO Auto-generated method stub return age; } @Override public String getFace () { // TODO Auto-generated method stub return face; } @Override public String getName () { // TODO Auto-generated method stub return name; } } boys . add( new ForeignerBoy ( " richale " , 28 , "美國" , "陽光" ));
設計原則是死的,也要根據實際的需求,我們要靈活使用這個開閉原則。
接口分離原則
定義:客戶端不應該依賴它不需要的接口,類間的依賴關係應該建立在最小的接口上。
public interface I { public void method1 (); public void method2 (); public void method3 (); } public class B implements I { @Override public void method1 () { System . out . println( "類B實現了接口I的方法1 " ); } @Override public void method2 () { System . out . println( "類B實現了接口I的方法2 " ); } @Override public void method3 () { //類B並不需要接口I的方法3功能,但是由於實現接口I,所以不得不實現方法3 //在這裡寫一個空方法 } } public class D implements I { @Override public void method2 () { System . out . println( "類D實現了接口I的方法2 " ); } @Override public void method3 () { System . out . println( "類D實現了接口I的方法3 " ); } @Override public void method1 () { //類D並不需要接口I的方法1功能,但是由於實現接口I,所以不得不實現方法1 //在這裡寫一個空方法 } } //類A通過接口I依賴類B public class A { public void depend1 ( I i ){ i . method1(); } } //類C通過接口I依賴類D public class C { public void depend1 ( I i ){ i . method3(); } } public class Client { public static void main ( String [] args ) { A a = new A (); I i1 = new B (); a . depend1(i1); C c = new C (); I i2 = new D (); c . depend1(i2); } }
運行結果:
類B實現了接口I的方法1 類D實現了接口I的方法3
從以上代碼可以看出,如果接口過於臃腫,不同業務邏輯的抽象方法都放在一個接口內,則會造成它的實現類必須實現自己並不需要的方法,這種設計方式顯然是不妥當的。所以我們要修改上述設計方法,把接口I拆分成3個接口,使得實現類只需要實現自己需要的接口即可。只貼出修改後的接口和實現類的代碼,修改代碼如下:
public interface I1 { public void method1 (); } public interface I2 { public void method2 (); } public interface I3 { public void method3 (); } public class B implements I1 , I2 { @Override public void method1 () { System . out . println( "類B實現了接口I的方法1 " ); } @Override public void method2 () { System . out . println( "類B實現了接口I的方法2 " ); } } public class D implements I2 , I3 { @Override public void method2 () { System . out . println( "類D實現了接口I的方法2 " ); } @Override public void method3 () { System . out . println( "類D實現了接口I的方法3 " ); } }
與單一職責原則的區別
到了這裡,有些人可能覺得接口隔離原則與單一職責原則很相似,其實不然。
-
單一職責原則注重的是職責;而接口隔離原則注重對接口依賴的隔離。
-
單一職責原則主要是約束類,其次才是接口和方法,它針對的是程序中的實現和細節;而接口隔離原則主要約束接口,主要針對抽象,針對程序整體框架的構建。
-
接口盡量小
-
接口高內聚
-
接口設計是有限度的
注意事項:
原則是軟件大師們經驗的總結,在軟件設計中具有一定的指導作用,但不能按部就班哈。對於接口隔離原則來說,接口盡量小,但是也要有限度。對接口進行細化可以提高程序設計靈活性是不爭的事實,但是如果過小,則會造成接口數量過多,使設計複雜化,所以一定要適度。
里氏代換原則
里氏代換原則是由麻省理工學院(MIT)計算機科學實驗室的Liskov女士,在1987年的OOPSLA大會上發表的一篇文章《Data Abstraction and Hierarchy》裡面提出來的,主要闡述了有關繼承的一些原則,也就是什麼時候應該使用繼承,什麼時候不應該使用繼承,以及其中的蘊涵的原理。2002年,軟件工程大師Robert C. Martin,出版了一本《Agile Software Development Principles Patterns and Practices》,在文中他把里氏代換原則最終簡化為一句話:"Subtypes must be substitutable for their base types" ,也就是說,子類必須能夠替換成它們的基類。
定義1:如果對每一個類型為T1的對象Object1,都有類型為T2 的對象Object2,使得以T1定義的所有程序P 在所有的對象Object1 都代換成Object2 時,程序P 的行為沒有發生變化,那麼類型T2 是類型T1 的子類型。
定義2:所有引用父類的地方必須能正常地使用其子類的對象。
問題由來:有一功能P1,由類Superclass完成。現需要將功能P1進行擴展,擴展後的功能為P,其中P由原有功能P1與新功能P2組成。新功能P由類Superclass的子類Subclass來完成,則子類B在完成新功能P2的同時,有可能會導致原有功能P1發生故障。
解決方案:當使用繼承時,遵循里氏替換原則。類Subclass繼承類Superclass時,除添加新的方法完成新增功能P2外,盡量不要重寫父類Superclass的方法,也盡量不要重載父類Superclass的方法。
不說沒用營養的了,上代碼:
public class Superclass { public int subtraction ( int a , int b ) { return a - b; } } public class Subclass extends Superclass { public int subtraction ( int a , int b ) { return a - b - 1 ; } } public class Client { public static void main ( String [] args ) { Sub class subclass = new Subclass(); SuperClass supuerClass = new SuperClass(); System.out.println("SuperClass + " + SuperClass.subtraction(10 - 5)); System.out.println("subclass + "+subclass.subtraction(10 - 5)); } }
大家看一下上面的代碼中子類可以代替父類,而不使原來的父類的計算功能不變嗎?答案是否定的。里氏替換原則通俗的來講就是:子類可以擴展父類的功能,但不能改變父類原有的功能。它包含以下4層含義:
- 子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
- 子類中可以增加自己特有的方法。
- 當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬鬆。
- 當子類的方法實現父類的抽象方法時,方法的後置條件(即方法的返回值)要比父類更嚴格。
依賴倒置原則
定義:高層模塊不應該依賴低層模塊,二者都應該依賴其抽象;抽像不應該依賴細節;細節應該依賴抽象。編程應該依賴抽象,不應該依賴細節。
問題由來:類A直接依賴類B,假如要將類A改為依賴類C,則必須通過修改類A的代碼來達成。這種場景下,類A一般是高層模塊,負責複雜的業務邏輯;類B和類C是低層模塊,負責基本的原子操作;假如修改類A,會給程序帶來不必要的風險。
解決方案:將類A修改為依賴接口I,類B和類C各自實現接口I,類A通過接口I間接與類B或者類C發生聯繫,則會大大降低修改類A的機率。
依賴倒置原則的核心思想是面向接口編程,我們依舊用一個例子來說明面向接口編程比相對於面向實現編程好在什麼地方。場景是這樣的,母親給孩子講故事,只要給她一本書,她就可以照著書給孩子講故事了。舉個例子說明一下:
class Book { public String getContent () { return "這個是一個關於毛主席的故事…… " ; } } class Mother { public void read ( Book book ) { System . out . println( "媽媽開始講故事" ); System . out . println(book . getContent()); } } public class Client { public static void main ( String [] args ) { Mother mother = new Mother (); mother . read( new Book ()); } }
運行結果:
媽媽開始講故事 這個是一個關於毛主席的故事……
運行良好,假如有一天,需求變成這樣:不是給書而是給一份報紙,讓這位母親講一下報紙上的故事,報紙的代碼如下:
class NewsPaper { public String getContent () { return "中國籃球超人姚明…… " ; } }
這位母親就辦不到,因為她居然不會讀報紙上的故事,這太荒唐了,只是將書換成報紙,居然必須要修改Mother才能讀。假如以後需求換成雜誌呢?換成網頁呢?還要不斷地修改Mother,這顯然不是好的設計。原因就是Mother與Book之間的耦合性太高了,必須降低他們之間的耦合度才行。
我們引入一個抽象的接口IReader。讀物,只要是帶字的都屬於讀物:
public Interface IReader { public String getContent(); }
Mother類與接口IReader發生依賴關係,而Book和Newspaper都屬於讀物的範圍,它們各自都去實現IReader接口,這樣就符合依賴倒置原則了,代碼修改為:
class Newspaper implements IReader { public String getContent () { return "中國籃球超人姚明…… " ; } } class Book implements IReader { public String getContent () { return "這個是一個關於毛主席的故事…… " ; } } class Mother { public void read ( IReader reader ) { System . out . println( "媽媽開始講故事" ); System . out . println(reader . getContent()); } } public class Client { public static void main ( String [] args ) { Mother mother = new Mother (); mother . read( new Book ()); mother . read( new Newspaper ()); } }
這樣以後,媽媽就是萬能的了,主要是讀物,媽媽都可以講給我了。
這樣修改後,無論以後怎樣擴展Client類,都不需要再修改Mother類了。這只是一個簡單的例子,實際情況中,代表高層模塊的Mother類將負責完成主要的業務邏輯,一旦需要對它進行修改,引入錯誤的風險極大。所以遵循依賴倒置原則可以降低類之間的耦合性,提高系統的穩定性,降低修改程序造成的風險。
採用依賴倒置原則給多人並行開髮帶來了極大的便利,比如上例中,原本Mother類與Book類直接耦合時,Mother類必須等Book類編碼完成後才可以進行編碼,因為Mother類依賴於Book類。修改後的程序則可以同時開工,互不影響,因為Mother與Book類一點關係也沒有。參與協作開發的人越多、項目越龐大,採用依賴導致原則的意義就越重大。現在很流行的TDD開發模式就是依賴倒置原則最成功的應用。
傳遞依賴關係有三種方式,以上的例子中使用的方法是接口傳遞,另外還有兩種傳遞方式:構造方法傳遞和setter方法傳遞,相信用過Spring框架的,對依賴的傳遞方式一定不會陌生。
在實際編程中,我們一般需要做到如下3點:
- 低層模塊盡量都要有抽像類或接口,或者兩者都有。
- 變量的聲明類型盡量是抽像類或接口。
- 使用繼承時遵循里氏替換原則。
依賴倒置原則的核心就是要我們面向接口編程,理解了面向接口編程,也就理解了依賴倒置。
迪米特原則
定義:也叫最少知道原則,如果兩個類不必彼此直接通信,那麼這兩個類就不應放生直接的互相作用。如果其中一個類需要調用另一個類的某個方法的化,可以用使用第三者轉發這個調用。
強調的前提:在類的結構設計上,每一個類都應當盡量降低成員的訪問權限,也就是說,一個類包裝好自己的private狀態,不需要讓別的類知道的字段或行為就不要公開。
根本思想是:強調類之間的松耦合,類之間的耦合越弱,越有利於復用,一個處在弱耦合的類被修改,不會對有關係的類造成波及。
public class A { public B getB ( String str ) { return new B (str); } public void work () { B b = getB( "李同學" ); C c = b . getC( "謝霆鋒" ); c . work(); } } public class B { public String name; public B () { } public B ( String name ) { this . name = name; } public C getC ( String name ) { return new C (name); } } public class C { public String name; public C ( String name ) { this . name = name; } public void work () { System . out . println(name + "幫我把這件事做好了" ); } } public class Client { public static void main ( String [] args ) { A a = new A ( "王同學" ); a . work(); } }
運行結果是:
謝霆鋒幫我把這件事做好了。
運行結果正常,但是我們發現一個問題,A類與B類有關聯,而A類與C類沒有什麼關聯。C類出現在A類中是不是有點不合時宜呢?看到這裡很多人都會明白,這種場景在實際開發中是非常常見的一種情況。對象A需要調用對象B的方法,對象B有需要調用對象C的方法……就是常見的
getXXX().getXXX().getXXX()
……類似於這種代碼。如果你發現你的代碼中也有這樣的代碼,那就考慮下是不是違反迪米特法則,是不是要重構一下了。
修改一下該例子:
public class A { pubic A (){ } public B getB ( String str ) { return new B (str); } public void work () { B b = getB( "王同學" ); b . work(); } } public class B { public String name; public B () { } public B ( String name ) { this . name = name; } public void work (){ C c = getC(“謝霆鋒”); c . work; } public C getC ( String name ) { return new C (name); } } public class C { public String name; public C ( String name ) { this . name = name; } public void work () { System . out . println(name + "幫我把這件事做好了" ); } } public class Client { public static void main ( String [] args ) { A a = new A ( "王同學" ); a . work(); } }
運行結果如下:
謝霆鋒幫我把這件事做好了
上面代碼只是修改了下類A和B的work方法,使之符合了迪米特法則:
- 類A只與最直接的朋友類B通信,不與類C通信;
- 類A只調用類B提供的方法即可,不用關心類B內部是如何實現的(至於B是怎麼調用的C,這些A都不用關心)。
優先使用組合,而不是使用繼承
定義:這個就不用解釋了吧,學習過面向對象編程的同學,都應該知道這個事。
** 組合:**
通過創建一個由其他對象組合的對象來獲得新功能的重用方法新功能的獲得是通過調用組合對象的功能實現的,有時又叫聚合。
例如: 一個對象擁有或者對另外一個對象負責並且兩個對像有相同的生命週期。(GOF) 一個對象包含另一個對象集合被包含對像對其他對像是不可見的並且只能從包含它的對像中訪問的特殊組合形式組合的優缺點
優點
- 被包含對象通過包含他們的類來訪問
- 黑盒重用,因為被包含對象的內部細節是不可見的
- 很好的封裝
- 每個類專注於一個任務
- 通過獲得和被包含對象的類型相同的對象引用,可以在運行時動態定義組合的方式
缺點
- 結果系統可能會包含更多的對象
- 為了使組合時可以使用不同的對象,必須小心的定義接口
繼承:
通過擴展已實現的對象來獲得新功能的重用方法基類有用通用的屬性和方法子類提供更多的屬性和方法來擴展基類
優點
- 新的實現很容易,因為大部分是繼承而來的
- 很容易修改和擴展已有的實現
缺點
- 打破了封裝,因為基類向子類暴露了實現細節
- 白盒重用,因為基類的內部細節通常對子類是可見的
- 當父類的實現改變時可能要相應的對子類做出改變
- 不能在運行時改變由父類繼承來的實現
由此可見,組合比繼承具有更大的靈活性和更穩定的結構,一般情況下應該優先考慮組合。只有當下列條件滿足時才考慮使用繼承:
- 子類是一種特殊的類型,而不只是父類的一個角色
- 子類的實例不需要變成另一個類的對象
- 子類擴展,而不是覆蓋或者使父類的功能失效
留言
張貼留言