uml面向對象分析與設計(概述耦合UML)
2023-04-15 19:22:45 1
迷茫了一周,一段時間重複的 CRUD ,著實讓我有點煩悶,最近打算將這些技術棧系列的文章先暫時擱置一下,開啟一個新的篇章《設計模式》,畢竟前面寫了不少 「武功招式」 的文章,也該提升一下內功了
一 設計模式概述(一) 什麼是設計模式設計模式,即Design Patterns,是指在軟體設計中,被反覆使用的一種代碼設計經驗。使用設計模式的目的是為了可重用代碼,提高代碼的可擴展性和可維護性
1995年,GoF(Gang of Four,四人組/四人幫)合作出版了《設計模式:可復用面向對象軟 件的基礎》一書,收錄了23種設計模式,從此樹立了軟體設計模式領域的裡程碑,【GoF設計模式】
(二) 為什麼學習設計模式前面我們學習了 N 種不同的技術,但是歸根結底,也只是 CRUD 與 調用之間的堆砌,或許這個創意亦或是業務很完善、很強大,其中也巧妙運用了各種高效的算法,但是說白了,這也只是為了實現或者說解決某個問題而做的
還有時候,兩個人同時開發一款相同的產品,均滿足了預期的需求,但是 A 的程序,不僅代碼健壯性強,同時後期維護擴展更是便捷(這種感覺,我們會在後面具體的設計模式中愈發的感覺到)而 B 的代碼卻是一言難盡啊
有一句話總結的非常好:
設計模式的本質是面向對象設計原則的實際運用,是對類的封裝性、繼承性和多態性以及類的關聯關係和組合關係的充分理解也就是說,畢竟像例如Java這樣面向對象的語言中,如何實現一個可維護,可維護的代碼,那必然就是要降低代碼耦合度,適當復用代碼,而要實現這一切,就需要充分的利用 OOP 編程的特性和思想
注:下面第二大點補充【耦合】的相關概念,若不需要跳轉第三四大點【UML類圖及類圖間的關係】/【設計模式七大原則】
在之前我寫 Spring依賴注入的時候【萬字長文】 Spring框架層層遞進輕鬆入門(0C和D),就是從傳統開發,講到了如何通過工廠模式,以及多例到單例的改進,來一步步實現解耦,有興趣的朋友可以看一下哈
【萬字長文】Spring框架 層層遞進輕鬆入門 (IOC和DI) juejin.im/post/684490…
二 什麼是耦合?(高/低)作為一篇新手都能看懂的文章,開始就一堆 IOC AOP等專業名詞扔出去,好像是不太禮貌,我得把需要鋪墊的知識給大家儘量說一說,如果對這塊比較明白的大佬,直接略過就OK了
耦合,就是模塊間關聯的程度,每個模塊之間的聯繫越多,也就是其耦合性越強,那麼獨立性也就越差了,所以我們在軟體設計中,應該儘量做到低耦合,高內聚
生活中的例子:家裡有一條串燈,上面有很多燈泡,如果燈壞了,你需要將整個燈帶都換掉,這就是高耦合的表現,因為燈和燈帶之間是緊密相連,不可分割的,但是如果燈泡可以隨意拆卸,並不影響整個燈帶,那麼這就叫做低耦合
代碼中的例子:來看一個多態的調用,前提是 B 繼承 A,引用了很多次
A a = new B;a.method;複製代碼
如果你想要把B變成C,就需要修改所有new B 的地方為 new C 這也就是高耦合
如果如果使用我們今天要說的 spring框架 就可以大大的降低耦合
A a = BeanFactory.getBean(B名稱);a.method;複製代碼
這個時候,我們只需要將B名稱改為C,同時將配置文件中的B改為C就可以了
常見的耦合有這些分類:
(一) 內容耦合當一個模塊直接修改或操作另一個模塊的數據,或者直接轉入另一個模塊時,就發生了內容耦合。此時,被修改的模塊完全依賴於修改它的模塊。 這種耦合性是很高的,最好避免
public class A { public int numA = 1;}public class B { public static A a = new A; public static void method{ a.numA = 1; } public static void main(String[] args) { method; System.out.println(a.numA); }}複製代碼
(二) 公共耦合
兩個以上的模塊共同引用一個全局數據項就稱為公共耦合。大量的公共耦合結構中,會讓你很難確定是哪個模塊給全局變量賦了一個特定的值
(三) 外部耦合一組模塊都訪問同一全局簡單變量,而且不通過參數表傳遞該全局變量的信息,則稱之為外部耦合 從定義和圖中也可以看出,公共耦合和外部耦合的區別就在於前者是全局數據結構,後者是全局簡單變量
(四) 控制耦合控制耦合 。一個模塊通過接口向另一個模塊傳遞一個控制信號,接受信號的模塊根據信號值而進行適當的動作,這種耦合被稱為控制耦合,也就是說,模塊之間傳遞的不是數據,而是一些標誌,開關量等等
(五) 標記耦合標記耦合指兩個模塊之間傳遞的是數據機構,如高級語言的數組名、記錄名、文件名等這些名字即為標記,其實傳遞的是這個數據結構的地址
(六) 數據耦合模塊之間通過參數來傳遞數據,那麼被稱為數據耦合。數據耦合是最低的一種耦合形 式,系統中一般都存在這種類型的耦合,因為為了完成一些有意義的功能,往往需要將某些模塊的輸出數據作為另 一些模塊的輸入數據
(七) 非直接耦合兩個模塊之間沒有直接關係,它們之間的聯繫完全是通過主模塊的控制和調用來實現的
三 UML 類圖及類圖之間的關係在一個相對完善的軟體系統中,每個類都有其責任,類與類之間,類與接口之間同時也存在著各種關係,UML(統一建模語言)從不同的角度定義了多種圖,在軟體建模時非常常用,下面我們說一下在設計模式中涉及相對較多的類圖,因為在後面單個設計模式的講解中,我們會涉及到,也算是一個基礎鋪墊。
(一) 類類是一組相關的屬性和行為的集合,是一個抽象的概念,在UML中,一般用一個分為三層的矩形框來代表類
第一層:類名稱,是一個字符串,例如 Student第二層:類屬性(欄位、成員變量)格式如下:[可見性]屬性名:類型[=默認值]例如:-name:String第三層:類操作(方法、行為),格式如下:[可見性]名稱(參數列表)[:返回類型]例如: display:void
(二) 接口接口,是一種特殊而又常用的類,不可被實例化,定義了一些抽象的操作(方法),但不包含屬性其實能見到接口 UML 描述的有三種形式:
第一種:使用一個帶有名稱的小圓圈來表示,上面的Dog是接口名,下面是接口定義的方法第二種:使用一個「框」來表示,和類很像,但是在最上面特別標註了 <>
(三) 關係(1) 依賴關係定義:如果一個元素 A 的變化影響到另一個元素 B,但是反之卻不成立,那麼這兩個元素 B 和 A 就可以稱為 B 依賴 A
例如:開門的人 想要執行開門這個動作,就必須藉助於鑰匙,這裡也就可以說,這個開門的人,依賴於鑰匙,如果鑰匙發生了什麼變化就會影響到開門的人,但是開門的人變化卻不會影響到鑰匙開門例如:動物生活需要氧氣、水分、食物,這就是一個很字面的依賴關係依賴關係作為對象之間耦合度最低的一種臨時性關聯方式
在代碼中,某個類的方法通過局部變量、方法的參數或者對靜態方法的調用來訪問另一個類(被依賴類)中的某些方法來完成一些職責。
(2) 關聯關係關聯就是類(準確的說是實例化後的對象)之間的關係,也就是說,如果兩個對象需要在一定時間內保持一定的關係,那麼就可以稱為關聯關係。
例如:學生(Student)在學校(School)學習知識(Knowledge)那麼這三者之間就存一個某種聯繫,可以建立關聯關係例如:大雁(WildGoose)年年南下遷徙,因為它知道氣候(climate)規律關聯關係的雙方是可以互相通訊的,也就是說,「一個類知道另一個類」
這種關聯是可以雙向的,也可以是單向的
雙向的關聯可以用帶兩個箭頭或者沒有箭頭的實線來表示單向的關聯用帶一個箭頭的實線來表示,箭頭從使用類指向被關聯的類也可以在關聯線的兩端標註角色名,代表兩種不同的角色在代碼中通常將一個類的對象作為另一個類的成員變量來實現關聯關係
下圖是一個教師和學生的雙向關聯關係
(3) 聚合關係聚合關係也稱為聚集關係,它是一種特殊的較強關聯關係。表示類(準確的說是實例化後的對象)之間整體與部分的關係,是一種 has-a 的關係
例如:汽車(Car)有輪胎(Wheel),Car has a Wheel,這就是一個聚合關係,但是輪胎(Wheel)獨立於汽車也可以單獨存在,輪胎還是輪胎聚合關係可以用帶空心菱形的實線箭頭來表示,菱形指向整體
(4) 組合關係組合是一種比聚合更強的關聯關係,其也表示類整體和部分之間的關係。但是整體對象可以控制部分對象的生命周期,一旦整體對象消失,部分也就自然消失了,即部分不能獨立存在
聚合關係可以用帶實心菱形的實線箭頭來表示,菱形指向整體
### (5) 泛化關係
泛化描述一般與特殊(類圖中「一般」稱為超類或父類,「特殊」稱為子類)的關係,是父類和子類之間的關係,是一種繼承關係,描述了一種 is a kind of 的關係,特別要說明的是,泛化關係式對象之間耦合度最大的一種關係
Java 中 extend 關鍵字就代表著這種關係,通常抽象類作為父類,具體類作為子類
例如:交通工具為抽象父類,汽車,飛機等就位具體的子類泛化關係用帶空心三角箭頭的實線來表示,箭頭從子類指向父類
(6) 實現關係實現關係就是接口和實現類之間的關係,實現類中實現了接口中定義的抽象操作
實現關係使用帶空心三角箭頭的虛線來表示,箭頭從實現類指向接口
四 設計模式七大原則(一) 開閉原則定義:軟體實體應當對擴展開放,對修改關閉
我們在開發任何產品的時候,別指望需求是一定不變的,當你不得不更改的你的代碼的時候,一個高質量的程序就體現出其價值了,它只需要在原來的基礎上增加一些擴展,而不至於去修改原先的代碼,因為這樣的做法常常會牽一髮而動全身。
也就是說,開閉原則要求我們在開發一個軟體(模塊)的時候,要保證可以在不修改原有代碼的模塊的基礎上,然後能擴展其功能
我們下面來具體談談
(1) 對修改關閉對修改關閉,即不允許在原來的模塊或者代碼上進行修改。
A:抽象層次
例如定義一個接口,不同的定義處理思路,會有怎樣的差別呢
定義一
boolean connectServer(String ip, int port, String user, String pwd)複製代碼
定義二
boolean connectServer(FTP ftp)複製代碼
public class FTP{ private String ip; private int port; private String user; private String pwd; ...... 省略 get set}複製代碼
兩種方式看似都是差不多的,也都能實現要求,但是如果我們想要在其基礎上增加一個新的參數
如果以定義一的做法,一旦接口被修改,所有調用 connectServer 方法的位置都會出現問題如果以定義二的做法,我們只需要修改 FTP 這個實體類,添加一個屬性即可這種情況下沒有用到這個新參數的調用處就不會出現問題,即使需要調用這個參數,我們也可以在 FTP 類的構造函數中,對其進行一個默認的賦值處理B:具體層次
對原有的具體層次的代碼進行修改,也是不太好的,雖然帶來的變化可能不如抽象層次的大,或者碰巧也沒問題,但是這種問題有時候是不可預料的,或許一些不經意的修改會帶了和預期完全不一致的結果
(2) 對擴展開放對擴展開放,也就是我們不需要在原代碼上進行修改,因為我們定義的抽象層已經足夠的合理,足夠的包容,我們只需要根據需求重新派生一個實現類來擴展就可以了
(3) 開發時如何處理無論模塊是多麼「封閉」,都會存在一些無法對之封閉的變化。既然不可能完全封閉,設計人員必須對他設計的模塊應該對那種變化封閉做出選擇,他必須先猜測出最有可能發現的變化種類,然後構造抽象來隔離那些變化 ——《大話設計模式》
預先猜測程序的變化,實際上是有很大難度,或許不完善,亦或者完全是錯誤的,所以為了規避這一點,我們可以選擇在剛開始寫代碼的時候,假設不會有任何變化出現,但當變化發生的時候,我們就要立即採取行動,通過 「抽象約束,封裝變化」 的方式,創建抽象來隔離發生的同類變化
舉例:
例如寫一個加法程序,很容易就可以寫的出來,這個時候變化還沒有發生
如果這個時候讓你增加一個減法或者乘除等的功能,你就發現,你就需要在原來的類上面修改,這顯然違背了 「開閉原則」,所以變化一旦發生,我們就立即採取行動,決定重構代碼,首先創建一個抽象類的運算類,通過繼承多態等隔離代碼,以後還想添加什麼類型的運算方式,只需要增加一個新的子類就可以了,也就是說,對程序的改動,是通過新代碼進行的,而不是更改現有代碼
小結:
我們希望開發剛開始就知道可能發生的變化,因為等待發現變化的時間越長,要抽象代碼的代價就越大不要刻意的去抽象,拒絕不成熟的抽象和抽象本身一樣重要(二) 裡氏替換原則(1) 詳細說明定義:繼承必須確保超類所擁有的性質在子類中仍然成立
裡氏替換原則,主要說明了關於繼承的內容,明確了何時使用繼承,亦或使用繼承的一些規定,是對於開閉原則中抽象化的一種補充
這裡我們主要談一下,繼承帶來的問題:
繼承是侵入性的,子類繼承了父類,就必須擁有父類的所有屬性和方法,降低了代碼靈活度耦合度變高,一旦父類的屬性和方法被修改,就需要考慮子類的修改,或許會造成大量代碼重構裡氏替換原則說簡單一點就是:它認為,只有當子類可以替換父類,同時程序功能不受到影響,這個父類才算真正被復用
其核心主要有這麼四點內容:
① 子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法② 子類中可以增加自己特有的方法③ 當子類的方法重載父類的方法時,子類方法的前置條件(即方法的輸入參數)要比父類的方法更寬鬆④ 當子類的方法實現父類的方法時(重寫/重載或實現抽象方法),方法的後置條件(即方法的的輸出/返回值)要比父類的方法更嚴格或相等對照簡單的代碼來看一下,就一目了然了
① 子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法
前半句很好理解,如果不實現父類的抽象方法,會編譯報錯
後半句是這裡的重點,父類中但凡實現好的方法,其實就是在設定整個繼承體系中的一系列規範和默認的契約,例如 鳥類 Bird 中,getFlyingSpeed(double speed) 用來獲取鳥的飛行速度,但幾維鳥作為一種特殊的鳥類,其實是不能飛行的,所以需要重寫繼承的子類方法 getFlyingSpeed(double speed) 將速度置為 0 ,但是會對整個繼承體系造成破壞
雖然我們平常經常會通過重寫父類方法來完成一些功能,同樣這樣也很簡單,但是一種潛在的繼承復用體系就被打亂了,如果在不適當的地方調用重寫後的方法,或多次運用多態,還可能會造成報錯
我們看下面的例子:
父類 Father
public class Father { public void speaking(String content){ System.out.println("父類: " content); }}複製代碼
子類 Son
public class Son extends Father { @Override public void speaking(String content) { System.out.println("子類: " content); }}複製代碼
子類 Daughter
public class Daughter extends Father{}複製代碼
測試類 Test
public class Test { public static void main(String[] args) { // 直接調用父類運行的結果 Father father = new Father; father.speaking("speaking方法被調用"); // Son子類替換父類運行的結果 Son son = new Son; son.speaking("speaking方法被調用"); // Daughter子類替換父類運行的結果 Daughter daughter = new Daughter; daughter.speaking("speaking方法被調用"); }}複製代碼
運行結果:
父類: speaking方法被調用 子類: speaking方法被調用 父類: speaking方法被調用
② 子類中可以增加自己特有的方法
這句話理解起來很簡單,直接看代碼
父類 Father
public class Father { public void speaking(String content){ System.out.println("父類: " content); }}複製代碼
子類 Son
public class Son extends Father { public void playGuitar { System.out.println("這是Son類playGuitar方法"); }}複製代碼
測試類 Test
public class Test { public static void main(String[] args) { // 直接調用父類運行的結果 Father father = new Father; father.speaking("speaking方法被調用"); // Son子類替換父類運行的結果 Son son = new Son; son.speaking("speaking方法被調用"); son.playGuitar; }}複製代碼
運行結果:
父類: speaking方法被調用 父類: speaking方法被調用 這是Son類playGuitar方法
③ 當子類的方法重載父類的方法時,子類方法的前置條件(即方法的輸入參數)要比父類的方法更寬鬆
這裡要注意,我們說的是重載,可不是重寫,下面我們按照裡氏替換原則要求的,將父類方法參數範圍設小一點 (ArrayList) ,將子類同名方法參數範圍寫大一些 (List) ,測試後的結果,就是只會執行父類的方法,不執行父類重載後的方法(註:參數名雖然相同,但是類型不同,還是重載,不是重寫)
父類 Father
public class Father { public void speaking(ArrayList arrayList) { System.out.println("父類: " arrayList.get(0)); }}複製代碼
子類 Son
public class Son extends Father { public void speaking(List list) { System.out.println("子類: " list.get(0)); }}複製代碼
測試類 Test
public class Test { public static void main(String[] args) { ArrayList arrayList = new ArrayList; arrayList.add("speaking方法被調用"); // 直接調用父類運行的結果 Father father = new Father; father.speaking(arrayList); // Son子類替換父類運行的結果 Son son = new Son; son.speaking(arrayList); }}複製代碼
運行結果:
父類: speaking方法被調用 父類: speaking方法被調用
如果我們將範圍顛倒一下,將父類方法參數範圍設大一些,子類方法參數設小一些,就會發現我明明想做的是重載方法,而不是重寫,但是父類的方法卻被執行了,邏輯完全出錯了,所以這也是這一條的反例,並不滿足裡氏替換原則
父類 Father
public class Father { public void speaking(List list) { System.out.println("父類: " list.get(0)); }}複製代碼
子類 Son
public class Son extends Father { public void speaking(ArrayList arrayList) { System.out.println("子類: " arrayList.get(0)); }}複製代碼
測試類 Test
public class Test { public static void main(String[] args) { ArrayList arrayList = new ArrayList; arrayList.add("speaking方法被調用"); // 直接調用父類運行的結果 Father father = new Father; father.speaking(arrayList); // Son子類替換父類運行的結果 Son son = new Son; son.speaking(arrayList); }}複製代碼
運行結果:
父類: speaking方法被調用 子類: speaking方法被調用
④ 當子類的方法實現父類的方法時(重寫/重載或實現抽象方法),方法的後置條件(即方法的的輸出/返回值)要比父類的方法更嚴格或相等
父類中定義一個抽象方法,返回值類型是 List,子類中重寫這個方法,返回值類型可以為 List,也可以更精確或更嚴格,例如 ArrayList
父類 Father
public abstract class Father { public abstract List speaking;}複製代碼
子類 Son
public class Son extends Father { @Override public ArrayList speaking { ArrayList arrayList = new ArrayList; arrayList.add("speaking方法被調用"); return arrayList; }}複製代碼
測試類 Test
public class Test { public static void main(String[] args) { Father father = new Son; System.out.println(father.speaking.get(0)); }}複製代碼
運行結果:
speaking方法被調用
但是,如果反過來,將父類抽象方法返回值定義為範圍較小的 ArrayList,將子類重寫方法中,反而將返回值類型方法,設置為 List,那麼程序在編寫的時候就會報錯
(2) 修正違背裡氏替換原則的代碼現在網上幾種比較經典的反例,「幾維鳥不是鳥」,「鯨魚不是魚」 等等
我打個比方,如果按照慣性和字面意思,如果我們將幾維鳥也繼承鳥類
但是幾維鳥是不能飛行的,所別的鳥通過 setSpeed 方法都能附一個有效的值,但是幾維鳥就不得不重寫這個 setSpeed 方法,讓其設置 flySpeed 為 0,這樣已經違反了裡氏替換原則
面對子類如果不能完整的實現父類的方法,或者父類的方法已經在子類中發生了「異變」,就例如這裡幾維鳥特殊的 setSpeed 方法,則一般選擇斷開父類和子類的繼承關係,重新設計關係
例如:
取消鳥和幾維鳥的繼承關係,定義鳥和幾維鳥更一般的父類,動物類
(三) 依賴倒置定義:
① 高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象② 抽象不應該依賴細節,細節應該依賴抽象先解釋第 ① 點,其實這一點在我們以往的分層開發中,就已經用過了,例如我們的業務層 Service(高層模塊)就沒有依賴數據訪問層 Dao/Mapper(低層模塊),我們都通過 Mapper 的接口進行訪問,這種情況下,如果數據訪問層的細節發生了變化,那麼也不會影響到業務層,但是如果直接依賴於實現,那麼就會影響巨大
第 ② 點,還是在討論要進行抽象的問題,抽象是高層,具體細節是底層,這和前一點也是契合的,正式說明了一條非常關鍵的原則 「面向接口編程,而非針對現實編程」
舉個例子
例如一個 Client 客戶想訪問學校的 readBook 方法,可以這麼寫
public class Client { public void read(ASchool aSchool){ System.out.println(aSchool.readBook); }}複製代碼
但是,這個地方其實就出現了一個比較大的問題,我們就是直接依賴了具體,而不是抽象,當我們想要查看另一個B學校的 readBook 方法,就需要將代碼修改為
public class Client { public void read(BSchool bSchool){ System.out.println(bSchool.readBook); }}複製代碼
但是開閉原則規定,對修改關閉,所以明顯違背了開閉原則,如果我們將代碼抽象出來,以接口訪問就可以解決
定義學校接口 ISchool (I 是大寫的 i 只是命名習慣問題,無特殊意義)
public interface ISchool { String readBook;}複製代碼
學校 A 和 B 分別實現這個接口,然後實現接口方法
public class ASchool implements ISchool { @Override public String readBook { return "閱讀《Java 編程思想》"; }}public class BSchool implements ISchool { @Override public String readBook { return "閱讀《代碼整潔之道》"; }}複製代碼
Client 客戶類,調用時,只需要傳入接口參數即可
public class Client { public void read(ISchool school){ System.out.println(school.readBook); }}複製代碼
看一下測試類
public class Test { public static void main(String[] args) { Client client = new Client; client.read(new ASchool); client.read(new BSchool); }}複製代碼
運行結果
閱讀《Java 編程思想》 閱讀《代碼整潔之道》
(四) 單一職責原則定義:單一職責原則規定一個類應該有且僅有一個引起它變化的原因,否則類應該被拆分
一個類,並不應該承擔太多的責任,否則當為了引入類中的 A 職責的時候,就不得不把 B 職責 也引入,所以我們必須滿足其高內聚以及細粒度
優點:
降低類的複雜度。一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單得多。提高類的可讀性。複雜性降低,自然其可讀性會提高。提高系統的可維護性。可讀性提高,那自然更容易維護了。變更引起的風險降低。變更是必然的,如果單一職責原則遵守得好,當修改一個功能時,可以顯著降低對其他功能的影響。就比如大學老師,負責很多很多工作,但是不管是輔導員,授課老師,行政老師,雖然都可以統稱為老師,但是將大量的內容和職責放到一個類中,顯然是不合理的,不如細分開來
例如:
補充:大家可能看過 「羊呼吸空氣,魚呼吸水」 的例子,這裡我不做演示,做一個說明,有時候,在類簡單的情況下,也可以在代碼或者方法級別上違背單一職責原則,因為即使一定的修改有一定開銷,但是幾乎可以忽略不計了,不過一般情況,我們還是要遵循單一職責原則
(五) 接口隔離原則定義:
客戶端不應該被迫依賴於它不使用的方法或者——客戶端不應該被迫依賴於它不使用的方法其實這一原則的核心就是 「拆」 ,如果在一個接口內存放過多的方法等內容,就會十分臃腫,竟可能的細化接口,也就是為每個類創建專用接口,畢竟依賴多個專用接口,比依賴一個綜合接口更加靈活方便,同時,接口作為對外的一個 「入口」,拆散,隔離接口能夠縮小外來因素導致的問題擴散範圍
還是通過一個例子來展開:
現在有一個 「好學生的接口和實現類」,還有一個老師的抽象類和其子類,老師能做的,就是去找到好的學生
好學生 IGoodStudent 接口
public interface IGoodStudent { //學習成績優秀 void goodGrades; //品德優秀 void goodMoralCharacter; //良好形象 void goodLooks;}複製代碼
好學生 IGoodStudent 接口的實現類 GoodStudentImpl
public class GoodStudentImpl implements IGoodStudent { private String name; public GoodStudentImpl(String name) { this.name = name; } @Override public void goodGrades { System.out.println("【" this.name "】的學習成績優秀"); } @Override public void goodMoralCharacter { System.out.println("【" this.name "】的品德優良"); } @Override public void goodLooks { System.out.println("【" this.name "】的形象良好"); }}複製代碼
老師抽象類 AbstractTeacher
public abstract class AbstractTeacher { protected IGoodStudent goodStudent; public AbstractTeacher(IGoodStudent goodStudent) { this.goodStudent = goodStudent; } public abstract void findGoodStudent;}複製代碼
老師類 Teacher
public class Teacher extends AbstractTeacher { public Teacher(IGoodStudent goodStudent) { super(goodStudent); } @Override public void findGoodStudent { super.goodStudent.goodGrades; super.goodStudent.goodMoralCharacter; super.goodStudent.goodLooks; }}複製代碼
測試類 Test
public class Test { public static void main(String[] args) { IGoodStudent goodStudent = new GoodStudentImpl("阿文"); AbstractTeacher teacher = new Teacher(goodStudent); teacher.findGoodStudent; }}複製代碼
運行結果:
【阿文】的學習成績優秀 【阿文】的品德優良 【阿文】的形象良好
一下子看來是沒什麼問題的,不過由於每個人的主觀意識形態不同,或許每個人對於 「好學生」 的定義並不同,就例如就我個人而言,我認識為 「師者,傳道授業解惑也」 ,學生能學習其為人處世的道理與主動學習更是難能可貴,至於外貌更屬於無稽之談。針對不同人的不同不同定義,這個 IGoodStudent 接口就顯得有一些龐大且不合時宜了,所以我們根據接口隔離原則,將 「好學生」 的定義進行一定的拆分隔離
學習的學生接口
public interface IGoodGradesStudent { //學習成績優秀 void goodGrades;}複製代碼
品德優秀的學生接口
public interface IGoodMoralCharacterStudent { //品德優秀 void goodMoralCharacter;}複製代碼
好學生實現多個接口
public class GoodStudent implements IGoodGradesStudent,IGoodMoralCharacterStudent { private String name; public GoodStudent(String name) { this.name = name; } @Override public void goodGrades { System.out.println("【" this.name "】的學習成績優秀"); } @Override public void goodMoralCharacter { System.out.println("【" this.name "】的品德優良"); }}複製代碼
(六) 迪米特法則
定義:如果兩個類不必要彼此直接通訊,那麼這兩個類就不應當發生直接的相互作用,如果其中一個類需要調用另一個類的某一個方法的話,可以通過第三者轉發這個調用
這句話的意思就是說,一個類對自己依賴的類知道越少越好,也就是每一個類都應該降低成員的訪問權限,就像封裝的概念中提到的,通過 private 隱藏自己的欄位或者行為細節
迪米特法則中的「朋友」是指:當前對象本身、當前對象的成員對象、當前對象所創建的對象、當前對象的方法參數等,這些對象同當前對象存在關聯、聚合或組合關係,可以直接訪問這些對象的方法
注意:請不要過分的使用迪米特法則,因為其會產生過多的中間類,會導致系統複雜性增大,結構不夠清晰
下面還是用一個例子來說一下
假設在學校的一個環境中,校長作為最高的職務所有人,肯定不會直接參與到對於老師和學生的管理中,而是通過一層一層的管理體系來進行統籌規劃,這裡的校長,和老師學生之間就可以理解為陌生關係,而校長和中層的教務主任卻是朋友關係,畢竟教務主任數量少,也可以直接進行溝通
教務主任類 AcademicDirector
public class AcademicDirector { private Principal principal; private Teacher teacher; private Student student; public void setPrincipal(Principal principal) { this.principal = principal; } public void setTeacher(Teacher teacher) { this.teacher = teacher; } public void setStudent(Student student) { this.student = student; } public void meetTeacher { System.out.println(teacher.getName "通過教務主任向" principal.getName "匯報工作"); } public void meetStudents { System.out.println(student.getName "通過教務主任與" principal.getName "見面"); }}複製代碼
校長類 Principal
public class Principal { private String name; Principal(String name) { this.name = name; } public String getName { return name; }}複製代碼
老師類 Teacher
public class Teacher { private String name; Teacher(String name) { this.name = name; } public String getName { return name; }}複製代碼
學生類 Student
public class Student { private String name; Student(String name) { this.name = name; } public String getName { return name; }}複製代碼
測試類 Test
public class Test { public static void main(String[] args) { AcademicDirector a = new AcademicDirector; a.setPrincipal(new Principal("【張校長】")); a.setTeacher(new Teacher("【王老師】")); a.setStudent(new Student("【阿文】")); a.meetTeacher; a.meetStudents; }}複製代碼
補充:迪米特法則在《程式設計師修煉之道》一書中也有提及到 —— 26 解耦與得墨忒耳法則
函數的得墨忒耳法則試圖使任何給定程序中的模塊之間的耦合減至最少,它設法阻止你為了獲得對第三個對象的方法的訪問而進入某個對象。
通過使用函數的得墨忒耳法則來解耦 編寫「羞怯」的代碼,我們可以實現我們的目標:
Minimize Coupling Between Modules
使模塊之間的耦合減至最少
(七) 合成復用原則定義:在軟體復用時,要儘量先使用組合或者聚合等關聯關係來實現,其次才考慮使用繼承關係來實現
這一點和裡氏替換原則的目的是一致的,都是處理關於繼承的內容,本質都是實現了開閉原則的具體規範
為什麼用組合/聚合,不用繼承
繼承破壞了類的封裝性,因為父類對於子類是透明的,而組合/聚合則不會繼承父子類之間之間的耦合度比組合/聚合新舊類高從父類繼承來的實現是靜態的,運行時不會發生變化,而組合/聚合的復用靈活性高,復用可在運行時動態進行如果代碼違背了裡氏替換原則,彌補的方式,一個就是我們前面說的,加入一個更普通的抽象超類,一個就是取消繼承,修改為組合/聚合關係
我們簡單回憶一下
繼承我們一般都叫做 Is-a 的關係,即一個類是另一個類的一種,比如,狗是一種動物組合/聚合都叫做 Has-a,即一個角色擁有一項責任或者說特性例如我們來討論一下常見的特殊自行車(即變速自行車),首先按照類型可以分為 山地自行車和公路自行車,按照速度搭配又可以分為 21速自行車 ,24速自行車,27速自行車(簡單分)
XX速山地自行/公路車,雖然說我們口頭上可能會這麼叫,但是其實這就是將速度這種 Has- a 的關係和 Is-a 的關係搞混了,而且如果通過繼承,會帶來很多的子類,一旦想要增加修改變速自行車種類以及速度類型,就需要修改原始碼,違背了開閉原則,所以修改為組合關係
五 結尾這篇文章寫到這裡就結束了,又是一篇 接近1W 字的內容,學習到一定階段,確實會有一些瓶頸,經過對於類似設計模式等 「內功」 的學習,也突然發現開發真不是 CRUD 的不斷重複,一段有質量的代碼,更能讓人有成就感,後面對於常見的設計模式我會一直更新下去,一邊學習,一邊總結,感謝大家的支持。
,