隨着軟件項目的經驗增加與深入,逐漸感覺到軟件在代碼上的冗余不斷提高與可維護性的降低,亟待軟件設計思想來指導我們的代碼,如何變得更加優美動人,使得軟件更加具有可維護性,可復用性,可拓展性,並達到軟件的高內聚低耦合目標。恰好的是,軟件設計模式,就是這樣一部經典的指導思想。以下,將展開對設計模式的六大原則(開閉原則、里氏代換原則、依賴倒轉原則、單一職責原則、接口隔離原則、迪米特法則)的解析。
一、開閉原則(Open-Closed-Principle)
核心:一個軟件實體應當對拓展開放,對修改關閉。即:軟件實體應盡量在不修改原有代碼的情況下進行拓展。
對Operation類的修改關閉,對Operation類的繼承(拓展)開放。
二、里氏代換原則(Liskow-Substitution-Principle)
核心:所有引用基類(父類)的地方,都必須能透明地使用其子類的對象。
1.一個軟件實體如果使用的是一個父類,那么一定適用於其子類,而且他察覺不出父類對象和子類對象的區別。也就是說,在軟件里面,吧父類都替換成它的子類,程序的行為沒有變化。簡單地說,子類新必須能夠替換掉它們的父類型。
Eg1:
我喜歡動物(父類)------(替換為)------->我喜歡狗(子類) 【√】
我喜歡狗(子類) ------(替換為)------->我喜歡動物(父類) 【×】
Eg2:
Animal animal = new Cat(); animal.eat(); animal.drink(); animal.run(); animel.shut();
注:需求的變化,使得需要將Cat更換為Dog,Snake,Sheep等別的動物,程序其他地方不需要改變。
Eg3:
class PersonUser{ usePerson(Person person){ ...;person.say(); } } abstract class Person{ abstract public void say(); } class Male extends Person{ @override public void say(){ System.out.println("I am a Male."); } } class Female extends Person{ @override public void say(){ System.out.println("I am a Female."); } } ======================== Client: PersonUser personUser = new PersonUser(); personUser.usePerson(new Male); personUser.usePerson(new Female); ======================== Output: I am Male. I am Female.
2.父類:抽象類/接口;子類:實現類
三、依賴倒轉原則(Dependency-Inversion-Principle)
核心:抽象不應該依賴於細節,細節應當依賴於抽象。換言之,要針對接口編程,而非針對實現編程。
思想:抽象不應該依賴於細節,細節應當依賴於抽象。換言之,要針對接口編程,而不是針對實現編程。依賴倒轉其實可以說是面向對象設計的標志,用哪種語言來編寫程序不重要,如果編寫時考慮的都是如何針對抽象編程而不是針對細節編程,即 程序中所有的依賴關系都是終止於抽象類或者接口,那就是面向對象的設計,反之就是面向過程化設計了。
原則:依賴倒轉原則:
1.高層模塊不應該依賴於低層模塊。兩個都應該依賴於抽象。
2.抽象不應該依賴細節。細節應該依賴於抽象。
問題由來:類A直接依賴類B,假如要將類A改為依賴類C,則必須通過修改類A的代碼來達成。這種場景下,類A一般是高層模塊,負責復雜的業務邏輯;類B和類C是低層模塊,負責基本的原子操作;假如修改類A,會給程序帶來不必要的風險。
解決方案:將類A修改為依賴接口I,類B和類C各自實現接口I,類A通過接口I間接與類B或者類C發生聯系,則會大大降低修改類A的幾率。
依賴倒置原則基於這樣一個事實:相對於細節的多變性,抽象的東西要穩定的多。以抽象為基礎搭建起來的架構比以細節為基礎搭建起來的架構要穩定的多。在java中,抽象指的是接口或者抽象類,細節就是具體的實現類,使用接口或者抽象類的目的是制定好規范和契約,而不去涉及任何具體的操作,把展現細節的任務交給他們的實現類去完成。
經典解說鏈接:【 設計模式六大原則(3):依賴倒置原則】
【防止目標網頁丟失,備份如下:】
場景:
(1)場景是這樣的,母親給孩子講故事,只要給她一本書,她就可以照着書給孩子講故事了。
class Book{ public String getContent(){ return "很久很久以前有一個阿拉伯的故事……"; } } class Mother{ public void narrate(Book book){ System.out.println("媽媽開始講故事"); System.out.println(book.getContent()); } } public class Client{ public static void main(String[] args){ Mother mother = new Mother(); mother.narrate(new Book()); } }
===========================================
output:
媽媽開始講故事
很久很久以前有一個阿拉伯的故事……
(2)運行良好,假如有一天,需求變成這樣:不是給書而是給一份報紙,讓這位母親講一下報紙上的故事,報紙的代碼如下:
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 implements IReader { public String getContent(){ return "林書豪17+9助尼克斯擊敗老鷹……"; } } class Book implements IReader{ public String getContent(){ return "很久很久以前有一個阿拉伯的故事……"; } } class Mother{ public void narrate(IReader reader){ System.out.println("媽媽開始講故事"); System.out.println(reader.getContent()); } } public class Client{ public static void main(String[] args){ Mother mother = new Mother(); mother.narrate(new Book()); mother.narrate(new Newspaper()); } }
===========================================
output:
媽媽開始講故事
很久很久以前有一個阿拉伯的故事……
媽媽開始講故事
林書豪17+9助尼克斯擊敗老鷹……
【依賴倒轉小結】
這樣修改后,無論以后怎樣擴展Client類,都不需要再修改Mother類了。這只是一個簡單的例子,實際情況中,代表高層模塊的Mother類將負責完成主要的業務邏輯,一旦需要對它進行修改,引入錯誤的風險極大。所以遵循依賴倒置原則可以降低類之間的耦合性,提高系統的穩定性,降低修改程序造成的風險。
采用依賴倒置原則給多人並行開發帶來了極大的便利,比如上例中,原本Mother類與Book類直接耦合時,Mother類必須等Book類編碼完成后才可以進行編碼,因為Mother類依賴於Book類。修改后的程序則可以同時開工,互不影響,因為Mother與Book類一點關系也沒有。參與協作開發的人越多、項目越龐大,采用依賴導致原則的意義就越重大。現在很流行的TDD開發模式就是依賴倒置原則最成功的應用。
傳遞依賴關系有三種方式,以上的例子中使用的方法是接口傳遞,另外還有兩種傳遞方式:構造方法傳遞和setter方法傳遞,相信用過Spring框架的,對依賴的傳遞方式一定不會陌生。
在實際編程中,我們一般需要做到如下3點:
- 低層模塊盡量都要有抽象類或接口,或者兩者都有。
- 變量的聲明類型盡量是抽象類或接口。
- 使用繼承時遵循里氏替換原則。
依賴倒置原則的核心就是要我們面向接口編程,理解了面向接口編程,也就理解了依賴倒置。
小結:開閉原則是目標,里氏代換是基礎,依賴倒轉是手段。
四、單一職責原則(Single-Responsibility-Principle)
核心:一個類只負責一個功能領域中相應的職責,或者可以定義為:就一個類而言,應該只有一個引起它變化的原因。
思想:如果一個類承擔的職責過多,就等於把這些職責耦合在一起,一個職責的變化可能會削弱或者抑制這個類完成其他職責的能力。這種耦合會導致脆弱的設計,當變化發生時,設計會遭受到意想不到的破壞。【Eg:游戲的界面組成與邏輯組成分離】
五、接口隔離原則(Interface-Segregation-Principle)
核心:使用多個專門的接口,而不使用單一的總接口。即 客戶端不應該依賴於那些它不需要的接口。
六、迪米特法則(Law-Of-Demeter)
核心:一個軟件實體應當盡可能少地與其他實體發生作用。(無熟人難辦事)
思想:也叫最少知識原則。如果兩個類不彼此通信,那么這兩個類就不應當直接地發生相互作用。如果其中一個類需要另一個類的某一個方法的話,可以通過第三者轉發這個調用。(不要和陌生人說話)
原則:
在迪米特法則中,對於一個對象,其朋友包括如下幾類:
(1)當前對象 this
(2)以參數形式傳入到當前對象方法中的對象
(3)當前對象的成員對象
(4)若當前對象的成員你對象是一個集合,那么集合中的對象也都是朋友
(5)當前對象所創建的對象
七、參考文獻
[2] 設計模式六大原則 - 迪米特法則
八、推薦書目
[1]《大話設計模式》【通俗易懂】
[2]《HeadFirst設計模式》【經典著作】
[3]《設計模式:可復用面向對象軟件的基礎》