關於面向對象的設計原則我以前已經解釋過四種了,分別是單一職責原則,開放關閉原則,裏式替換原則,依賴倒置原則而接下來咱們要解釋的就是最後的三種原則了,分別是接口隔離原則, 迪米特法則, 組合複用原則。編程
前言設計模式
在面向對象的軟件設計中,只有儘可能下降各個模塊之間的耦合度,才能提升代碼的複用率,系統的可維護性、可擴展性才能提升。面向對象的軟件設計中,有23種經典的設計模式,是一套前人代碼設計經驗的總結,若是把設計模式比做武功招式,那麼設計原則就比如是內功心法。經常使用的設計原則有七個,下文將具體介紹。bash
設計原則簡介ide
單一職責原則:專一下降類的複雜度,實現類要職責單一;工具
開放關閉原則:全部面向對象原則的核心,設計要對擴展開發,對修改關閉;ui
裏式替換原則:實現開放關閉原則的重要方式之一,設計不要破壞繼承關係;spa
依賴倒置原則:系統抽象化的具體實現,要求面向接口編程,是面向對象設計的主要實現機制之一;設計
接口隔離原則:要求接口的方法儘可能少,接口儘可能細化;3d
迪米特法則:下降系統的耦合度,使一個模塊的修改儘可能少的影響其餘模塊,擴展會相對容易;code
組合複用原則:在軟件設計中,儘可能使用組合/聚合而不是繼承達到代碼複用的目的。
這些設計原則並不說咱們必定要遵循他們來進行設計,而是根據咱們的實際狀況去怎麼去選擇使用他們,來讓咱們的程序作的更加的完善。
接口隔離原則
定義:
接口隔離原則常常略寫爲ISP,講的是使用多個專門的接口比使用單一的接口要好。
換句話來講從一個客戶類的角度來講,一個類對另一個類的依賴性應當是創建在最小的接口上的。
那麼到底該怎麼去理解這個接口隔離原則呢?
我以爲能夠從三個方面去理解這個事情。
1. 角色的合理劃分
將「接口」理解爲一個類所提供的全部的方法的特徵集合,也就是一種在邏輯上才存在的概念,這樣的話,接口的劃分其實就是直接在類型上的劃分。
其實能夠這麼想,一個接口就至關於劇本中的一個角色,而這個角色在表演的過程當中,決定由哪個演員來進行表演就至關因而接口的實現,所以,一個接口表明的應當是一個角色而不是多個角色,若是系統涉及到多個角色的話,那麼每個角色都應當由一個特定的接口表明纔對。
而爲了不咱們產山混淆的想法,這時候咱們就能夠把接口隔離原則理解成角色隔離原則。
2. 定製服務
將接口理解成咱們開發中狹義的JAVA接口的話,這樣子,接口隔離原則講的就是爲同一個角色提供寬窄不一樣的接口,來應對不一樣的客戶端內容,我畫一個簡單的圖示,你們就徹底能明白了。
上面這個辦法其實就能夠稱之爲定製服務,在上面的圖中有一個角色service以及三個不一樣的客戶端,這三個Client須要的服務是不同的,因此我給他分紅了是三個接口,也就是Service1,Service2和Service3,顯而易見,每個JAVA接口,都僅僅是將Cilent須要的行爲暴露給Client,而沒有將不須要的方法暴露出去。
其實瞭解設計模式的很容易就想到這是適配器模式的一個應用場景,我不細聊適配器模式,設計模式咱們在知識星球中會進行講解。
3. 接口污染
這句話的意思就是過於臃腫的接口就是對接口的污染。
因爲每個接口都表明一個角色,實現一個接口對象,在他的整個生命週期中,都扮演着這個角色,所以將角色分清就是系統設計的一個重要的工做。所以一個符合邏輯的判斷,不該該是將幾個不一樣的角色都交給一個接口,而是應該交給不一樣的接口來進行處理。
準確而恰當的劃分角色以及角色所對應的接口,就是咱們面向對象設計中的一個重要的組成部分,若是將沒有關係或者關係不大的接口整合到一塊兒去的話,那就是對角色和接口的污染。
咱們來寫代碼來舉個例子論證一下:
public interface TestInterface { public void method1(); public void method2(); public void method3(); public void method4(); public void method5();}class Test1 { public void mm1(TestInterface i) { i.method1(); } public void mm2(TestInterface i) { i.method2(); } public void mm3(TestInterface i) { i.method3(); }}class Test2 implements TestInterface{ @Override public void method1() { System.out.println("類Test2實現接口TestInterface的方法1"); } @Override public void method2() { System.out.println("類Test2實現接口TestInterface的方法2"); } @Override public void method3() { System.out.println("類Test2實現接口TestInterface的方法3"); } @Override public void method4() {} @Override public void method5() {}}class Test3{ public void mm1(TestInterface i) { i.method1(); } public void mm2(TestInterface i) { i.method4(); } public void mm3(TestInterface i) { i.method5(); }}class Test4 implements TestInterface{ @Override public void method1() { System.out.println("類Test4實現接口TestInterface的方法1"); } @Override public void method2() { } @Override public void method3() { } @Override public void method4() { System.out.println("類Test4實現接口TestInterface的方法4"); } @Override public void method5() { System.out.println("類Test4實現接口TestInterface的方法5"); }}而後咱們看一下調用方式public class Client { public static void main(String[] args) { Test1 test1 = new Test1(); test1.mm1(new Test2()); test1.mm2(new Test2()); test1.mm3(new Test2()); Test3 test3 = new Test3(); test3.mm1(new Test4()); test3.mm2(new Test4()); test3.mm3(new Test4()); }}複製代碼
執行結果以下:
類Test2實現接口TestInterface的方法1類Test2實現接口TestInterface的方法2類Test2實現接口TestInterface的方法3類Test4實現接口TestInterface的方法1類Test4實現接口TestInterface的方法4類Test4實現接口TestInterface的方法5複製代碼
很顯然,這不遵循接口設計原則,那麼咱們怎麼去設計遵循接口設計原則的代碼呢?
public interface TestInterface1 { public void method1();}interface TestInterface2{ public void method2(); public void method3();}interface TestInterface3 { public void method4(); public void method5();}class Test1{ public void mm1(TestInterface1 i){ i.method1(); } public void mm2(TestInterface2 i){ i.method2(); } public void mm3(TestInterface2 i){ i.method3(); }}class Test2 implements TestInterface1,TestInterface2{ @Override public void method1() { System.out.println("類Test2實現接口TestInterface1的方法1"); } @Override public void method2() { System.out.println("類Test2實現接口TestInterface2的方法2"); } @Override public void method3() { System.out.println("類Test2實現接口TestInterface2的方法3"); }}class Test3{ public void mm1(TestInterface1 i){ i.method1(); } public void mm2(TestInterface3 i){ i.method4(); } public void mm3(TestInterface3 i){ i.method5(); }}class Test4 implements TestInterface1,TestInterface3{ @Override public void method1() { System.out.println("類Test4實現接口TestInterface1的方法1"); } @Override public void method4() { System.out.println("類Test4實現接口TestInterface3的方法4"); } @Override public void method5() { System.out.println("類Test4實現接口TestInterface3的方法5"); }}複製代碼
接口隔離原則的含義是:創建單一接口,不要創建龐大臃腫的接口,儘可能細化接口,接口中的方法儘可能少。也就是說,咱們要爲各個類創建專用的接口,而不要試圖去創建一個很龐大的接口供全部依賴它的類去調用。
我寫的這個例子中,將一個龐大的接口變動爲3個專用的接口所採用的就是接口隔離原則。
因此其實接口隔離原則其實也算是「看人下菜碟」,它的意思就是要看客人是誰,在提供不一樣檔次的飯菜。
從接口隔離原則的角度出發的話,要根據客戶不一樣的需求,去指定不一樣的服務,這就是接口隔離原則中推薦的方式。
迪米特法則
定義:
迪米特法則(Law of Demeter)又叫做最少知識原則(Least Knowledge Principle 簡寫LKP),就是說一個對象應當對其餘對象有儘量少的瞭解,不和陌生人說話。英文簡寫爲: LoD。
其實他主要是爲了解決一個咱們最多見的問題,就是類之間的關係,因此類與類之間的關係越密切,耦合度就越大,當一個類放生改變的時間,對另外一個類的影響也會越大。
而他最終的解決方案就是下降類和類之間的耦合度,這也是咱們所說的高內聚,低耦合。
咱們來經過簡單的一個系統的代碼來理解一下迪米特法則。
不知足迪米特法則的系統
這裏的系統有三個類,分別是SomeOne,Friend和Stranger。其中SomeOne與Friend是朋友,而Friend和Stranger是朋友,系統結構圖就像下面的。
從上面的類圖中,咱們能夠看到,Friend持有Stranger對象的引用,這就解釋了爲何Friend與Stranger是朋友,咱們給出一點代碼來解釋SomeOne和Friend的朋友關係。
public class Someone{ public void operation1(Friend friend){ Stranger stranger = friend.provide(); stranger.operation3(); }}複製代碼
能夠看出,SomeOne具備一個方法operation1(),這個方法接收friend爲參數,顯然,根據朋友的定義,Friend和Stranger是朋友關係,其中的Friend的provide()方法會提供本身所建立的Stranger實例,就像下面的代碼
public class Friend{ private Stranger stranger = new Stranger(); public void operation2(){ } public Stranger provide(){ return stranger; }}複製代碼
這其實就很顯然了,SomeOne的方法operation1()並不知足迪米特法則,爲何會這麼說呢?由於這個方法引用Stranger對象,而Stranger對象不是SomeOne的朋友。
咱們下面使用迪米特法則來進行改造一下這個關係和代碼。
使用迪米特法則進行改造
從上面的圖中咱們能夠看出,與改造以前相比,在SomeOne與Stranger之間的聯繫已經沒有了,SomeOne不須要知道Stranger的存在就能夠作一樣的事情,咱們看一下SomeOne的代碼,
public class Someone{ public void operation1(Friend friend){ friend.forward(); }}複製代碼
從源代碼中咱們能夠看出,SomeOne經過調用本身的朋友Friend對象的forward()方法作到了原來須要調用Stranger對象纔可以作到的事情,那麼咱們再來看一下forward()方法是作什麼呢?
public class Frend{ private Stranger stranger = new Stranger(); public void operation2(){ System.out.printIn("In Friend.operation2()"); } public void forward(){ stranger.operation3(); }}複製代碼
原來Friend類的forward()方法所作的就是之前SomeOne要作的事情,使用Stranger的operation3()方法,而這種forward()方法叫作轉發方法,
因爲使用了調用轉發,使得調用的具體的細節被隱藏在Friend內部,從而使SomeOne與Stranger之間的直接聯繫被省略掉了,這樣一來,系統內部的耦合度下降了,在系統的某一個類須要修改時,僅僅會影響這個類的「朋友們」,而不會直接影響到其餘的部分。
以上就是我對迪米特法則的一些理解,有興趣的人也能夠去深刻研究一下,未來對理解設計模式會有很好的看法的。
組合複用原則
定義:
組合複用原則常常又叫作合成複用原則。該原則就是在一個新的對象裏面使用一些已有的對象,使之成爲新對象的一部分:新的對象經過向這些對象的委派達到複用已有功能的目的。
而在咱們的代碼中儘量使用組合而不是用繼承是什麼緣由呢?
緣由以下
第一,繼承複用破壞包裝,它把父類的實現細節直接暴露給了子類,這違背了信息隱藏的原則;
第二:若是父類發生了改變,那麼子類也要發生相應的改變,這就直接致使了類與類之間的高耦合,不利於類的擴展、複用、維護等,也帶來了系統僵硬和脆弱的設計。而用合成和聚合的時候新對象和已有對象的交互每每是經過接口或者抽象類進行的,就能夠很好的避免上面的不足,並且這也可讓每個新的類專一於實現本身的任務,符合單一職責原則。
其實這個組合複用原則最好的理解就是咱們各類系統中的後臺系統裏面的權利和角色的分配,我相信不少公司的項目中都會有,我來闡述一下這個問題。
「Has-A」和「Is-A」
「Is-A」是嚴格的分類學意義上的定義,意思是一個類是另一個類的「一種」。而「Has-A」則不一樣,他表示某一個角色具備某一項責任。
咱們看一個圖解
人被繼承到「僱員」,「經理」,「學生」等子類,而實際上,「僱員」,「經理」,「學生」分別描述一種角色,而「人」能夠同時有幾種不一樣的角色,好比,一個「人」即便「經理」,就必然是「僱員」,而有可能這個「人」仍是一個「學生」。若是說使用繼承來講,那麼若是這我的是「學生」,那麼它必定不能再是經理,這個你們能夠思考一下爲何,很簡單,這顯然就是不合理的。
圖中的這種就是把「角色」的等級結構和「人」的等級結構混淆了,把「Has-A」角色誤解成爲了「Is-A」角色,而下面這幅圖就成功的解釋了這一點
而在這個圖中,就不存在以前混淆的問題了,每一個人均可以擁有一個以上的「角色」了。
組合/聚合複用原則使用總結:
合成和聚合均是關聯的特殊狀況。聚合用來表示「擁有」關係或者總體與部分的關係;而合成則用來表示一種強得多的「擁有」關係。在一個合成關係裏面,部分和總體的生命週期是同樣的。一個合成的新的對象徹底擁有對其組成部分的支配權,包括它們的建立和銷燬等。使用程序語言的術語來講,組合而成的新對象對組成部分的內存分配、內存釋放有絕對的責任。要正確的選擇合成/複用和繼承,必須透徹地理解里氏替換原則和Coad法則。(Coad法則由Peter Coad提出,總結了一些何時使用繼承做爲複用工具的條件。Coad法則:只有當如下Coad條件所有被知足時,才應當使用繼承關係)
1. 子類是基類的一個特殊種類,而不是基類的一個角色。區分「Has-A」和「Is-A」。只有「Is-A」關係才符合繼承關係,「Has-A」關係應當用聚合來描述。
2. 永遠不會出現須要將子類換成另一個類的子類的狀況。若是不能確定未來是否會變成另一個子類的話,就不要使用繼承。
3. 子類具備擴展基類的責任,而不是具備置換掉(override)或註銷掉(Nullify)基類的責任。若是一個子類須要大量的置換掉基類的行爲,那麼這個類就不該該是這個基類的子類。
4. 只有在分類學角度上有意義時,纔可使用繼承。不要從工具類繼承。
以上就是我最後介紹的關於設計模式以前的設計原則的全部了,三篇文章,你學會了麼?
我是懿,一個正在被打擊還在努力前進的碼農。歡迎你們關注咱們的公衆號,加入咱們的知識星球,咱們在知識星球中等着你的加入。