我來講兩句
- 觀看更多精彩文章訪問主頁:https://blog.csdn.net/weixin_45692705?spm=1011.2124.3001.5343
- 獲取文章資料教程請關注下方
公眾號, - 想要看懂原始碼寫出好的代碼,學習設計模式是必要的,在這給大家推薦一本好書(只是推薦因為我也在看,不是打廣告大家買不買跟我也沒有一毛錢關系 ●’?’●),
- 大家踴躍的點贊評論一下,最近因為作業原因更新也很慢了,希望發布的作品都能上熱榜吧,

好了下面我們開始正文 😊
Java面向物件設計原則文章目錄
- 我來講兩句
- 面向物件設計原則概述
- 一.軟體的可維護性和可復用性
- 二.面向物件設計原則
- 單一職責原則
- 開閉原則
- 里氏替換原則
- 依賴倒轉原則
- 介面隔離原則
- 合成復用原則
- 迪米特法則
- 總結
面向物件設計原則概述
- 面向物件設計原則是學習設計模式的基礎,每一種設計模式都符合某一種或多種面向物件設計原則,通過在軟體開發中使用這些原則,可以提高軟體的可維護性和可復用性,讓我們可以設計出更加靈活也更容易擴展的軟體系統,實作可維護性復用的目標,

一.軟體的可維護性和可復用性
- 通常認為,一個易于維護的系統就是復用率高的系統,而一個復用性較好的系統就是一個易于維護的系統,但實際上軟體的
可維護性(Maintainability)和可復用性(Reusability)是兩個獨立的目標,對于面向物件的軟體系統設計來說,在支持可維護性的同時提高系統的可復用性是一個核心問題,面向物件設計原則正是為解決這個問題而誕生的,
一個可維護性較低的軟體設計通常由如下幾個原因造成的
過于僵硬
- 很難在一個軟體系統中添加一個新的功能,增加一個新的功能將涉及很多模塊,造成系統改動較大,如在源代碼中存在大量的硬編碼,使得代碼的靈活性很差,幾乎所有的修改都要面向程式源代碼進行,
過于脆弱
- 與過于僵硬同時存在,修改已有系統時代碼過于脆弱﹐對一個地方的修改會導致看上去沒有關系的另一個地方發生故障,
復用率低
- 復用是指一個軟體的組成部分可以在同一個專案的不同地方甚至在不同的專案中重復使用,而復用率低表示很難重用這些現有的軟體組成部分,如類、方法、子系統等﹐即使是重用也只停留在簡單的復制粘貼上,甚至根本沒有辦法重用,程式員寧愿不斷重復撰寫一些已有的程式代碼,
黏度過高
- 對系統進行改動時,有時候可以保存系統的原始設計意圖和原始設計框架,有時候可以破壞原始意圖和框架,前者對系統的擴展更有利,應該盡量按照前者來進行改動,如果采用后者比前者更容易,則稱為系統的黏度過高﹐黏度過高將導致程式員采用錯誤的代碼維護方案,
了解完 “可維護性較低的軟體設計” 我們再來看看一款好的系統設計應該具備哪些性質
可擴展性
- 容易將新的功能添加到現有系統中,與“過于僵硬”相對應,
靈活性
- 代碼修改時不會波及很多其他模塊,與“過于脆弱"相對應,
可插入性
- 可以很方便地將一個類抽取出去,同時將另一個有相同介面的類添加進來,與“黏度過高”相對應,
這有個疑問如何使得系統滿足上述的三個性質呢?

- 其關鍵在于恰當提高系統的可維護性和可復用性,軟體的復用(Reuse)或重用擁有眾多優點,如可以提高軟體的開發效率,提高軟體質量,節約開發成本,恰當的復用還可以改善系統的可維護性,
下面我們來舉例說明 👇
-
傳統的軟體復用技術包括代碼的復用、演算法的復用和資料結構的復用等,但這些復用有時候會破壞系統的可維護性,因為可維護性和可復用性是有共性的兩個獨立質量屬性,如A和B兩個模塊都需要使用另一個模塊C,如果A需要C增加一個新的行為,但B不需要甚至不允許C增加該行為,如果堅持使用復用,就不得不以系統的可維護性為代價,如修改B的代碼,這將破壞系統的靈活性,而如果從保持系統的可維護性出發,就只好放棄復用,而面向物件設計復用在一定程度上可以解決這兩個質量屬性之間發生沖突的問題,
-
面向物件設計復用的目標在于實作支持可維護性的復用,如在Java這樣的語言中,可以通過面向物件技術中的抽象,繼承,封裝和多型等特性來實作更高層次的可復用性,通過抽象和繼承使得類的定義可以復用,通過多型使得類的實作可以復用,通過抽象和封裝可以保持和促進系統的可維護性,在面向物件的設計里面,可維護性復用都是以面向物件設計原則為基礎的,這些設計原則首先都是復用的原則,遵循這些設計原則可以有效地提高系統的復用性,同時提高系統的可維護性,
-
面向物件設計原則和設計模式也是對系統進行合理重構的指南針,重構(Refactoring)是在不改變軟體現有功能的基礎上,通過調整程式代碼改善軟體的質量﹑性能,使其程式的設計模式和架構更趨合理,提高軟體的擴展性和維護性,
二.面向物件設計原則
- 常用的面向物件設計原則包括7個,這些原則并不是孤立存在的,它們相互依賴、相互補充,
| 名稱 | 介紹 |
|---|---|
| 單一職責原則 | 類的職責要單一,不能將太多的職責放在一個類中 |
| 開閉原則 | 軟體物體對擴展是開放的,但對修改是關閉的,即在不修改一個軟體物體的基礎上去擴展其功能 |
| 里氏替換原則 | 在軟體系統中,一個可以接受基類物件的地方必然可以接受一個子類物件 |
| 依賴倒轉原則 | 要針對抽象層編程,而不要針對具體類編程 |
| 介面隔離原則 | 使用多個專門的介面來取代一個統一的介面 |
| 合成復用原則 | 在復用功能時,應該盡量多使用組合和聚合關聯關系,盡量少使用甚至不使用繼承關系 |
| 迪米特法則 | 一個軟體物體對其他物體的參考越少越好,或者說如果兩個類不必彼此直接通信,那么這兩個類就不應當發生直接的相互作用,而是通過引人一個第三者發生間接互動 |
- 下面我們將帶大家逐個了解面向物件中用到的七大原則 🌹
單一職責原則
- 單一職責原則是最簡單的面向物件設計原則,它用于控制類的粒度大小,
單一職責原則定義
- 一個物件應該只包含單一的職責,并且該職責被完整地封裝在一個類中,另一種定義就一個類而言,應該僅有一個引起它變化的原因,
單一職責原則分析
- 一個類(或者大到模塊,小到方法)承擔的職責越多,它被復用的可能性越小,而且如果一個類承擔的職責過多,就相當于將這些職責耦合在一起,當其中一個職責變化時,可能會影響其他職責的運作,
- 類的職責主要包括兩個方面:資料職責和行為職責,資料職責通過其屬性來體現,而行為職責通過其方法來體現,如果職責太多,將導致系統非常脆弱,一個職責可能會影響其他職責,因此要將這些職責進行分離,將不同的職責封裝在不同的類中,即將不同的變化原因封裝在不同的類中,如果多個職責總是同時發生改變,則可將它們封裝在同一類中,
- 單一職責原則是實作高內聚﹑低耦合的指導方針,在很多代碼重構手法中都能找到它的存在,它是最簡單但又最難運用的原則,需要設計人員發現類的不同職責并將其分離,而發現類的多重職責需要設計人員具有較強的分析設計能力和相關重構經驗,
單一職責原則實體
- 下面通過一個簡單實體來加深對單一職責原則的理解,
實體說明
- 基于Java的C/S系統的“登錄功能"通過如下登錄類(Login)實作,如下圖所示
在上方類圖中省略了類的屬性,Login類的方法說明如下:

init()方法用于初始化按鈕、文本框等界面控制元件;display()方法用于向界面容器中增加界面控制元件并顯示視窗;validate()方法供登錄按鈕的事件處理方法呼叫,用于呼叫與資料庫相關的方法完成登錄處理,如果登錄成功則進入主界面﹐否則提示錯誤資訊;getConnection()方法用于獲取資料庫連接物件Connection來連接資料庫;findUser()方法用于根據用戶名和密碼查詢資料庫中是否存在該用戶,如果存在則回傳true,否則回傳false,該方法需要呼叫getConnection()方法連接資料庫,并供 validate()方法呼叫;main()函式是系統的主函式,即系統的入口,
現在使用單一職責原則對其進行重構
實體決議
- 在本實體中,類Login承擔了多重職責,它既包含了與界面有關的方法,又包含了與資料庫操作有關的方法,甚至還包含了系統的入口函式 main()方法,無論是對界面的修改還是對資料庫訪問的修改都需要修改該類,類的職責過重,如果另一個系統(如B/S系統)也需要使用該類中的資料訪問代碼進行登錄,無法直接重用這些資料訪問代碼,只能復制粘貼部分代碼,無法實作高層次的復用,
根據單一職責原則,可以對上述代碼進行重構,按照功能將其拆分為如下4個類(還可以進一步拆分):
- 類LoginForm負責界面顯示,因此它只包含與界面有關的方法和事件處理方法;
- 類 UserDAO負責用戶表的增刪改查操作,它封裝了對用戶表的全部操作代碼,登錄本質上是一個查詢用戶表的操作;
- 類DBUtil負責資料庫的連接,該類可以供多個資料庫操作類重用,所有操作資料庫的類都可以呼叫該類中的getConnection()方法來獲取資料庫連接物件;
- 類MainClass負責啟動系統,在該類中定義了main()函式,
重構后的類圖

- 通過單一職責原則重構后將使得系統中類的個數增加,但是類的復用性很好,如上圖中, DBUtil類可供多個DAO類使用,而UserDAO類也可供多個界面類使用,一個類的修改不會對其他類產生影響,系統的可維護性也將增強,
開閉原則
- 開閉原則是面向物件的可復用設計的第一塊基石,它是最重要的面向物件設計原則,
開閉原則定義
- 一個軟體物體應當對擴展開放,對修改關閉,也就是說在設計一個模塊的時候,應當使這個模塊可以在不被修改的前提下被擴展,即實作在不修改源代碼的情況下改變這個模塊的行為,
開閉原則分析
- 開閉原則由Bertrand Meyer于 1988年提出,它是面向物件設計中最重要的原則之一,在開閉原則的定義中,軟體物體可以指一個軟體模塊、一個由多個類組成的區域結構或一個獨立的類,
- 任何軟體都需要面臨一個很重要的問題,即對它們的需求會隨時間的推移而發生變化,當軟體系統需要面對新的需求時,我們應該盡量保證系統的設計框架是穩定的,如果一個軟體設計符合開閉原則,那么可以非常方便地對系統進行擴展,而且在擴展時無須修改現有代碼,使得軟體系統在擁有適應性和靈活性的同時具備較好的穩定性和延續性,
- 為了滿足開閉原則,需要對系統進行抽象化設計,抽象化是開閉原則的關鍵,在類似Java,C#的面向物件編程語言中,可以為系統定義一個相對穩定的抽象層,而將不同的實作行為在具體的實作層中完成,在很多面向物件編程語言中都提供了介面、抽象類等機制,可以通過它們定義系統的抽象層,再通過具體類來進行擴展,如果需要修改系統的行為,無須對抽象層進行任何改動,只需要增加新的具體類來實作新的業務功能即可,實作在不修改已有代碼的基礎上擴展系統的功能,達到開閉原則的要求,
- 開閉原則還可以通過一個更加具體的 “對可變性封裝原則” 來描述,對可變性封裝原則(Principle of Encapsulation of Variation,EVP)要求找到系統的可變因素并將其封裝起來,如將抽象層的不同實作封裝到不同的具體類中,而且EVP要求盡量不要將一種可變性和另一種可變性混合在一起,這將導致系統中類的個數急劇增長,增加系統的復雜度,
- 百分之百的開閉原則很難達到,但是要盡可能使系統設計符合開閉原則,后面所學的里氏代換原則、依賴倒轉原則等都是開閉原則的實作方法,在即將學習的24種設計模式中,絕大部分的設計模式都符合開閉原則,在對每一個模式進行優缺點評價時都會以開閉原則作為一個重要的評價依據,以判斷基于該模式設計的系統是否具備良好的靈活性和可擴展性,
開閉原則實體
- 下面通過一個簡單實體來加深對開閉原則的理解
實體說明
- 某圖形界面系統提供了各種不同形狀的按鈕﹐客戶端代碼可針對這些按鈕進行編程,用戶可能會改變需求,要求使用不同的按鈕,原始設計方案如下圖所示,

- 如果界面類LoginForm需要將圓形按鈕(CircleButton)改為矩形按鈕(RectangleButton),則需要修改LoginForm類的源代碼,修改按鈕類的類名,由于圓形按鈕和矩形按鈕的顯示方法不相同,因此還需要修改LoginForm類的display(方法實作代碼,
現對該系統進行重構,使之滿足開閉原則的要求,
實體決議
-
分析上述實體,由于LoginForm類面向具體類進行編程,因此每次更換具體類時不得不修改源代碼,而且在這些具體類中方法沒有統一的介面,相似功能的方法名稱不一致,如果希望系統能夠滿足開閉原則,需要對按鈕類進行抽象化,提取一個抽象按鈕類AbstractButton,LoginForm類針對抽象按鈕類AbstractButton進行編程,在Java語言中,可以通過組態檔、DOM決議技術和反射機制將具體類類名存盤在組態檔中,再在運行時生成其實體物件,
-
使用開閉原則對本實體進行重構后,LoginForm類將面向抽象進行編程,如果需要增加新的按鈕類如菱形按鈕(Diamond Button),只需要增加一個新的類繼承抽象類AbstractButton并修改組態檔(如 config. xml)即可,無須修改已有類的源代碼,包括抽象層類AbstractButton,具體按鈕類CircleButton和 RectangleButton,以及使用按鈕的界面類LoginForm 的源代碼,在不修改源代碼的前提下擴展系統功能的要求,完全符合開閉原則,在Java 中,組態檔一般使用XML格式的檔案或properties格式的屬性檔案,如下圖所示,

注意:因為XML 和 properties等格式的組態檔是純文本檔案,可以直接通過VI編輯器或記事本進行編輯,且無須編譯,因此在軟體開發中,一般不把對組態檔的修改認為是對系統源代碼的修改,如果一個系統在擴展時只涉及修改組態檔,而原有的Java代碼或C#代碼沒有做任何修改,該系統即可認為是一個符合開閉原則的系統,
里氏替換原則
- 開閉原則的核心是對系統進行抽象化,并且從抽象化匯出具體化,從抽象化到具體化的程序需要使用繼承關系以及本節將要學習的里氏代換原則,
里氏替換原則定義
里氏代換原則(Liskov Substitution Principle,LSP)有兩種定義方式
-
第一種定義方式相對嚴格:如果對每一個型別為S的物件o1,都有型別為T的物件o2,使得以丁定義的所有程式Р在所有的物件o1都代換o2時,程式Р的行為沒有變化,那么型別S是型別T的子型別,
-
第二種是更容易理解的定義方式:所有參考基類(父類)的地方必須能透明地使用其子類的物件,
里氏替換原則分析
- 里氏代換原則可以通俗表述為:在軟體中如果能夠使用基類物件,那么一定能夠使用其子類物件,把基類都替換成它的子類,程式將不會產生任何錯誤和例外,反過來則不成立,如果一個軟體物體使用的是一個子類的話,那么它不一定能夠使用基類,
- 例如有兩個類,一個類為 BaseClass,另一個是SubClass類,并且SubClass類是BaseClass類的子類,那么一個方法如果可以接受一個 BaseClass型別的基類物件base的話,如 method1 ( base),那么它必然可以接受一個 BaseClass型別的子類物件sub,即method1 (sub)能夠正常運行,反過來的代換不成立,如方法 method2接受BaseClass型別的子類物件sub為引數(即 method2(sub))后,則一般情況下不可以有 method2(base),除非是多載方法,
- 里氏代換原則是實作開閉原則的重要方式之一,由于使用基類物件的地方都可以使用子類物件,因此在程式中盡量使用基型別別來對物件進行定義,而在運行時再確定其子型別別,用子類物件來替換父類物件,
在使用里氏替換原則時需要注意如下幾個問題:
- 子類的所有方法必須在父類中宣告,或子類必須實作父類中宣告的所有方法,根據里氏替換原則,為了保證系統的擴展性,在程式中通常使用父類來進行定義,如果一個方法只存在子類中,父類中不提供相應的宣告,則無法在父類物件中直接使用該方法,如果在父類 BaseClass 中宣告了方法 method1() ,在子類SubClass中實作了方法 method1(),并增加了新的方法 method2() ,如果客戶端針對父類編程,則無法使用子類中新增方法 method2() ,此時無法直接使用父類來定義,只能使用子類,則說明該設計違背了里氏替換原則,需要在設計父類時宣告方法 method2(),以確保客戶端可以透明地使用父類和子類物件,
- 在運用里氏替換原則時,盡量把父類設計為抽象類或者介面,讓子類繼承父類或實作父介面,并實作在父類中宣告的方法,運行時,子類實體替換父類實體,我們可以很方便地擴展系統的功能,同時無須修改原有子類的代碼,增加新的功能可以通過增加一個新的子類來實作,里氏替換原則是開閉原則的具體實作手段之一,
- Java語言中,在編譯階段,Java編譯器會檢查一個程式是否符合里氏替換原則,這是一個與實作無關的,純語法意義上的檢查,但Java編譯器的檢查是有局限的,
里氏替換原則實體
- 下面通過一個簡單實體來加深對里氏代換原則的理解,
實體說明
- 某系統需要實作對重要資料(如用戶密碼)的加密處理,在資料操作類(DataOperator)中需要呼叫加密類中定義的加密演算法,系統提供了兩個不同的加密類CipherA和CipherB,它們實作不同的加密方法,在 DataOperator中可以選擇其中的一個實作加密操作,如圖下圖所示,

- 在DataOperator類的encrypt(O)方法中,將呼叫加密類CipherA 或CipherB的加密方法encrypt(),如在客戶類Client的 main()函式中可能存在如下代碼片段:
CipherA cipherA = new CipherA();
DataOperator do = new DataOperator();
do.setCipherA(cipherA);
...
- 與之對應,在:DataOperator類的encryptO)方法中可能存在如下代碼片段:
...
return cipherA.encrypt(plainText)
- 如果需要更換一個加密演算法類或者增加并使用一個新的加密演算法類,如將上述CipherA改為CipherB,則需要修改客戶類Client和資料操作類DataOperator的源代碼,違背了開閉原則,
現使用里氏替換原則對其進行重構,使得系統可以靈活擴展,符合開閉原則,
實體決議
- 在本實體中,導致系統靈活性和可擴展性差的本質原因是Client類和 DataOperator類都針對每一個具體類進行編程,每增加一個具體類都將修改源代碼,此時,可以將CipherB作為CipherA的子類,Client類和 DataOperator類都針對CipherA進行編程,根據里氏代換原則,所有能夠接受CipherA類物件的地方都可以接受CipherB類的物件,因此可以簡化DataOperator類和Client類的代碼,而且將CipherA類物件替換成CipherB類物件很方便,無須修改任何源代碼,如果需要增加一個新的加密演算法類,如CipherC,只須將CipherC類作為CipherA類或CipherB類的子類即可,重構后的類圖如下圖所示,

-
在上圖中,由于CipherB是CipherA的子類,因此所有能夠使用CipherA物件的地方都可以使用CipherB物件來替換,且可以將具體類的類名存盤至組態檔中,如果需要使用CipherA 的encrypt()方法﹐則組態檔中存盤的類名為CipherA,如果需要使用CipherB的encrypt()方法,則組態檔中存盤的類名為CipherB,
-
如果需要增加一個新的加密類﹐如CipherC,則可將CipherC繼承CipherA或CipherB,并覆寫其中定義的encrypt()方法,并將組態檔中存盤的類名改為CipherC,所有現有類的代碼無須做任何改變,完全符合開閉原則,
依賴倒轉原則
- 如果說開閉原則是面向物件設計的目標的話,那么依賴倒轉原則就是實作面向物件設計的主要機制,依賴倒轉原則是系統抽象化的具體實作,
依賴倒轉原則定義
-
高層模塊不應該依賴低層模塊,它們都應該依賴抽象,抽象不應該依賴于細節,細節應該依賴于抽象,
-
另一種表述:要針對介面編程,不要針對實作編程,
依賴倒轉原則分析
-
簡單來說,依賴倒轉原則就是指:代碼要依賴于抽象的類,而不要依賴于具體的類﹔要針對介面或抽象類編程,而不是針對具體類編程,也就是說,在程式代碼中傳遞引數時或在組合聚合關系中,盡量參考層次高的抽象層類,即使用介面和抽象類進行變數類型宣告,引數型別宣告,方法回傳型別宣告,以及資料型別的轉換等,而不要用具體類來做這些事情,為了確保該原則的應用,一個具體類應當只實作介面和抽象類中宣告過的方法,而不要給出多余的方法,否則將無法呼叫到在子類中增加的新方法,
-
實作開閉原則的關鍵是抽象化,并且從抽象化匯出具體化實作,如果說開閉原則是面向物件設計的目標的話,那么依賴倒轉原則就是面向物件設計的主要手段,有了抽象層,可以使得系統具有很好的靈活性,在程式中盡量使用抽象層進行編程,而將具體類寫在組態檔中,這樣一來,如果系統行為發生變化,只需要擴展抽象層,并修改組態檔,而無須修改原有系統的源代碼,在不修改的情況下來擴展系統的功能,滿足開閉原則的要求,依賴倒轉原則是COM,CORBA,EJB、Spring 等技術和框架背后的基本原則之一,
下面簡單介紹一下依賴倒轉原則中經常提到的兩個概念,類之間的耦合和依賴注入,
1.類之間的耦合
在面向物件系統中,兩個類之間通常可以發生三種不同的耦合關系(依賴關系),
-
零耦合關系:如果兩個類之間沒有任何耦合關系,稱為零耦合,
-
具體耦合關系:具體耦合發生在兩個具體類(可實體化的類)之間,由一個類對另一個具體類實體的直接參考產生,
-
抽象耦合關系:抽象耦合關系發生在一個具體類和一個抽象類之間,也可以發生在兩個抽象類之間,使兩個發生關系的類之間存有最大的靈活性,由于在抽象耦合中至少有一端是抽象的,因此可以通過不同的具體實作來進行擴展,
依賴倒轉原則要求客戶端依賴于抽象耦合,以抽象方式耦合是依賴倒轉原則的關鍵,
由于一個抽象耦合關系總要涉及具體類從抽象類繼承,并且需要保證在任何參考到基類的地方都可以替換成其子類,因此,里氏代換原則是依賴倒轉原則的基礎,
2.依賴注入
- 物件與物件之間的依賴關系是可以傳遞的,通過傳遞依賴,在一個物件中可以呼叫另一個物件的方法,在傳遞時要做好抽象依賴,針對抽象層編程,簡單來說,依賴注入就是將一個類的物件傳入另一個類,注入時應該盡量注入父類物件,而在程式運行時再通過子類物件來覆寫父類物件,依賴注入有以下三種方式,
1.構造注入
- 構造注入是通過建構式注入實體變數,代碼如下:
public interface AbstractBook
{
public void view();
}
public interface AbstractReader
{
public void read();
}
public class ConcreteBook implements AbstractBook
{
public void view(){
...
}
}
public class ConcreteReader implements AbstractReader
{
private AbstractBook book;
public ConcreteReader(AbstractBook book){
this.book = book;
}
public void read(){
book.view();
}
}
2.設值注入
- 設值注入是通過Setter方法注入實體變數,代碼如下:
public interface AbstractBook
{
public void view();
}
public interface AbstractReader
{
public void setBook(AbstractBook book);
public void read();
}
public class ConcreteBoak implements AbstractBook
{
public void view(){
...
}
}
public class ConcreteReader implements AbstractReader
{
private AbstractBook book;
public void setBook(AbstractBook book){
this.book = book;
}
public void read(){
book.view();
}
}
2.介面注入
- 介面注入是通過介面方法注人實體變數,代碼如下:
public interface AbstractBook
{
public void view();
}
public interface AbstractReader
{
public void view(){
...
}
}
public class ConcretReader implements AbstractReader
{
public void read(AbstractBook book){
book.view();
}
}
依賴倒轉實體
- 下面通過一個簡單實體來加深對依賴倒轉原則的理解,
實體說明
- 某系統提供一個資料轉換模塊,可以將來自不同資料源的資料轉換成多種格式,如可以轉換來自資料庫的資料(DatabaseSource),也可以轉換來自文本檔案的資料(TextSource),轉換后的格式可以是XMI檔案(XMI.Transformer),也可以是XL.S檔案( XLSTransformer)等,
某設計人員設計如下原始類圖,用于實作該資料轉換模塊,如下圖所示,

- 由于需求的變化,該系統可能需要增加新的資料源或者新的檔案格式,每增加一個新的型別的資料源或者新的型別的檔案格式,客戶類 MainClass都需要修改源代碼,以便使用新的類,違背了開閉原則,現使用依賴倒轉原則對其進行重構,
實體決議
- 在本實體中,MainClass類針對具體類編程,如果增加新的具體類必須修改 MainClass類的源代碼,系統的可擴展性和靈活性受到局限﹐因此可以對這些具體類進行抽象化,使得 MainClass類針對抽象層進行編程,而將具體類放在組態檔中,重構后的系統類圖如下圖所示,

- 在上圖中,引入了兩個抽象類(或介面)AbstractSource和AbstractTransformer,MainClass依賴于這兩個抽象類,針對抽象類進行編程,而將具體類類名存盤在組態檔config.xml中,通過XML決議技術和Java反射機制生成具體類的實體,代換 MainClass類中的抽象物件,實作真正的業務處理,在這個程序中使用了里氏代換原則,依賴倒轉原則必須以里氏代換原則為基礎,增加新的資料源或檔案格式時,只需要增加一個AbstractSource或 AbstractTransformer類的子類,同時修改config.xml 組態檔,更換具體類類名,無須對原有類的代碼進行任何修改,滿足開閉原則的要求,
介面隔離原則
- 介面隔離原則要求我們將一些較大的介面進行細化,使用多個專門的介面來替換單一的總介面,
介面隔離原則的定義
- 客戶端不應該依賴那些它不需要的介面,
注意,在該定義中的介面指的是所定義的方法,- 另一種定義,一旦一個介面太大,則需要將它分割成一些更細小的介面,使用該介面的客戶端僅需知道與之相關的方法即可,
介面隔離原則分析
-
實質上,介面隔離原則是指使用多個專門的介面,而不使用單一的總介面,每一個介面應該承擔一種相對獨立的角色,不多不少,不干不該干的事,該干的事都要干,這里的“介面”往往有兩種不同的含義:一種是指一個型別所具有的方法特征的集合,僅僅是一種邏輯上的抽象﹔另外一種是指某種語言具體的“介面”定義,有嚴格的定義和結構,如Java語言里面的interface,對于這兩種不同的含﹐ISP的表達方式以及含義都有所不同,
-
當把“介面”理解成一個型別所提供的所有方法特征的集合的時候,這就是一種邏輯上的概念,介面的劃分將直接帶來型別的劃分,此時,可以把介面理解成角色,一個介面就只代表一個角色,每個角色都有它特定的一個介面,此時這個原則可以叫做“角色隔離原則”,
-
如果把“介面”理解成狹義的特定語言的介面,那么ISP表達的意思是指介面僅僅提供客戶端需要的行為,即所需的方法,客戶端不需要的行為則隱藏起來,應當為客戶端提供盡可能小的單獨的介面,而不要提供大的總介面,在面向物件編程語言中,如果需要實作一個介面,就需要實作該介面中定義的所有方法,因此大的總介面使用起來不一定很方便,為了使介面的職責單一,需要將大介面中的方法根據其職責不同分別放在不同的小介面中,以確保每個介面使用起來都較為方便,并都承擔某一單一角色,介面應該盡量細化,同時介面中的方法應該盡量少,每個介面中只包含一個客戶端(如子模塊或業務邏輯類)所需的方法即可,
-
使用介面隔離原則拆分介面時,首先必須滿足單一職責原則,將一組相關的操作定義在一個介面中,且在滿足高內聚的前提下﹐介面中的方法越少越好,可以在進行系統設計時采用定制服務的方式,即為不同的客戶端提供寬窄不同的介面,只提供用戶需要的行為,而隱藏用戶不需要的行為,
介面隔離原則實體
- 下面通過一個簡單的實體來加深對介面隔離原則的理解,
實體說明
- 下圖展示了一個擁有多個客戶類的系統,在系統中定義了一個巨大的介面AbstractService來服務所有的客戶類,

- 如果客戶類ClientA只須針對方法 operatorA()進行編程,但由于提供的是一個胖介面,AbstractService的實作類ConcreteService必須實作在AbstractService中宣告的所有三個方法,而且在ClientA中除了能夠看到方法 operatorA() ,還能夠看到與之不相關的方法operatorB()和 operatorC() ,在一定程度上影響系統的封裝性,因此,可以使用介面隔離原則對其進行重構,
實體決議
- 由于在介面AbstractService中三個不同的方法分別對應三類不同的客戶端,因此需要將該介面進行細化,以確保每一類用戶都具有與之對應的專門的介面,可以將該介面分割成三個小介面,如圖下圖所示,

- 通過對AbstractService介面的細化,我們可以將其分割為三個專門的介面;AbstractServiceA ,A bstractServiceB和AbstractServiceC,在每個介面中只包含一個方法,用于對應一個客戶端,在實際使用程序中,如果一個客戶端對應多個方法,可以將這幾個方法封裝在同一個小介面中,介面實作類ConcreteService可以一次性實作這三個介面,也可以提供三個介面實作類分別實作這三個介面,無論是使用一個實作類還是使用三個實作類,對于ClientA等客戶端類而言沒有任何區別,因為它們是針對抽象的介面編程,只能看到與自己相關的業務方法,不能訪問其他方法,因此保證系統具有良好的封裝性,同時,無須關心一個業務方法的改變會給一些不相關的類造成影響,因為這些類根本無法訪問該方法,
- 在使用介面隔離原則時需要注意介面的粒度,介面不能太小,如果太小會導致系統中介面泛濫,不利于維護,介面也不能太大,太大的介面將違背介面隔離原則,靈活性較差,使用起來很不方便,一般而言,介面中僅包含為某一類用戶定制的方法即可,
合成復用原則
- 合成復用原則是面向物件設計中非常重要的一條原則,為了降低系統中類之間的耦合度,該原則倡導在復用功能時多用關聯關系,少用繼承關系,
合成復用原則定義
- 合成復用原則(Composite Reuse Principle,CRP)又稱為組合/聚合復用原則(Composition/Aggregate Reuse Principle,CARP),其定義為:
盡量使用物件組合,而不是繼承來達到復用的目的,
合成復用原則分析
- GoF提倡在實作復用時更多考慮用物件組合機制,而不是用類繼承機制,通俗地說,合成復用原則就是指在一個新的物件里通過關聯關系(包括組合關系和聚合關系)來使用一些已有的物件,使之成為新物件的一部分﹔新物件通過委派呼叫已有物件的方法達到復用其已有功能的目的,簡言之,要盡量使用組合/聚合關系,少用繼承,
在面向物件設計中,可以通過兩種基本方法在不同的環境中復用已有的設計和實作,即通過組合/聚合關系或通過繼承,這兩種復用機制的特點如下:
- 通過繼承來實作復用很簡單,而且子類可以覆寫父類的方法,易于擴展,但其主要問題在于繼承復用會破壞系統的封裝性,因為繼承會將基類的實作細節暴露給子類,由于基類的某些內部細節對子類來說是可見的,所以這種復用又稱為“白箱”復用,如果基類發生改變,那么子類的實作也不得不發生改變;從基類繼承而來的實作是靜態的,不可能在運行時發生改變,沒有足夠的靈活性;而且繼承只能在有限的環境中使用(例如類不能被宣告為final類),
- 通過組合/聚合來復用是將一個類的物件作為另一個類的物件的一部分,或者說一個物件是由另一個或幾個物件組合而成,由于組合或聚合關系可以將已有的物件(也可稱為成員物件)納人到新物件中,使之成為新物件的一部分,因此新物件可以呼叫已有物件的功能,這樣做可以使得成員物件的內部實作細節對于新物件是不可見的,所以這種復用又稱為“黑箱”復用,相對繼承關系而言,其耦合度相對較低,成員物件的變化對新物件的影響不大,可以在新物件中根據實際需要有選擇性地呼叫成員物件的操作﹔合成復用可以在運行時動態進行,新物件可以動態地參考與成員物件型別相同的其他物件,
- 組合/聚合可以使系統更加靈活,類與類之間的耦合度降低,一個類的變化對其他類造成的影響相對較少,因此一般首選使用組合/聚合來實作復用,其次才考慮繼承,在使用繼承時,需要嚴格遵循里氏代換原則,有效使用繼承會有助于對問題的理解,降低復雜度,而濫用繼承反而會增加系統構建和維護的難度以及系統的復雜度,因此需要慎重使用繼承復用,
合成復用原則實體
- 下面通過一個簡單實體來加深對合成復用原則的理解,
實體說明
- 某教學管理系統的部分資料庫訪問類設計如下圖所示,

- 在該類圖中, DBUtil類用于連接資料庫,它提供了一個 getConnection()方法,用于回傳一個Connection型別的資料庫連接物件,由于在StudentDAO、TeacherDAO等類中都需要連接資料庫,因此需要復用getConnection()方法,在本設計方案中, StudentDAO、TeacherDAO等資料訪問類直接繼承 DBUtil 類,復用其中定義的方法,
- 如果需要更換資料庫連接方式,如原來采用JDBC連接資料庫,現在采用資料庫連接池連接,則需要修改 DBUtil類源代碼,如果StudentDAO采用JDEC連接,但是TeacherDAO采用連接池連接,則需要增加一個新的 DBUtil類,并修改StudentDAO或TeacherDAO的源代碼,使之繼承新的資料庫連接類,這將違背開閉原則,系統擴展性較差,
下面使用合成復用原則對其進行重構
實體決議
- 根據合成復用原則,我們可以使用組合/聚合復用來取代繼承復用,如下圖所示,

- StudentDAO和TeacherDAO類與DBUril 類不再是繼承關系,而改為聚合關聯關系,并增加一個setDBOperator()方法來給DBUtil型別的成員變數dBOperator賦值,如果需要改為另一種資料庫連接方式,只需要給DBUtil 增加一個子類,如NewDBUtil,在該子類中覆寫getConnection()方法,再在客戶類中呼叫setDBOperator()方法時注入子類物件即可,如果希望系統更加靈活一點,可以在客戶類中針對 DBUtil編程,而將具體類類名存盤在組態檔中,DBUtil類及其子類都可以直接應用于該系統,用戶無須修改任何源代碼,只需修改組態檔即可完成新的資料庫連接方式的使用,完全符合開閉原則,
迪米特法則
- 迪米特法則用于降低系統的耦合度,使類與類之間保持松散的耦合關系,
迪米特法則定義
迪米特法則(Law of Demeter,LoD)又稱為最少知識原則(Least Knowledge Principle,LKP),它有多種定義方法,其中幾種典型定義如下:
- 不要和 “陌生人” 說話,
- 只與你的直接朋友通信,
- 每一個軟體單位對其他的單位都只有最少的知識,而且局限于那些與本單位密切相關的軟體單位,
迪米特法則分析
- 簡單地說,迪米特法則就是指一個軟體物體應當盡可能少地與其他物體發生相互作用,這樣,當一個模塊修改時,就會盡量少地影響其他的模塊,擴展會相對容易,這是對軟體物體之間通信的限制,它要求限制軟體物體之間通信的寬度和深度,
在迪米特法則中,對于一個物件,其朋友包括以下幾類:
- 當前物件本身
this; - 以引數形式傳入到當前物件方法中的物件;
- 當前物件的成員物件;
- 如果當前物件的成員物件是一個集合,那么集合中的元素也都是朋友;
- 當前物件所創建的物件,
任何一個物件如果滿足上面的條件之一,就是當前物件的“朋友”,否則就是“陌生人”,迪米特法則可分為狹義法則和廣義法則,在狹義的迪米特法則中,如果兩個類之間不必彼此直接通信,那么這兩個類就不應當發生直接的相互作用,如果其中的一個類需要呼叫另一個類的某一個方法的話,可以通過第三者轉發這個呼叫,如圖下圖所示,

-
在上圖中,Object A 與Object B存在依賴關系,ObjectC是Object B的成員物件,根據迪米特法則,Object A只能呼叫ObjectB中的方法,而不允許呼叫Object C中的方法,因為它們之間不存在直接參考關系,根據迪米特法則,不允許出現a.method1(). method2()或者a. b. method()這樣的呼叫方式,只允許出現 a. method(),也就是在方法呼叫時只能夠出現一個“.”(點號),
-
狹義的迪米特法則可以降低類之間的耦合,但是會在系統中增加大量的小方法并散落在系統的各個角落,它可以使一個系統的區域設計簡化,因為每一個區域都不會和遠距離的物件有直接的關聯,但是也會造成系統的不同模塊之間的通信效率降低,使得系統的不同模塊之間不容易協調, -
廣義的迪米特法則就是指對物件之間的資訊流量﹑流向以及資訊的影響的控制,主要是對資訊隱藏的控制,資訊的隱藏可以使各個子系統之間脫耦,從而允許它們獨立地被開發、優化,使用和修改,同時可以促進軟體的復用,由于每一個模塊都不依賴于其他模塊而存在,因此每一個模塊都可以獨立地在其他的地方使用,一個系統的規模越大,資訊的隱藏就越重要,而資訊隱藏的重要性也就越明顯,
迪米特法則的主要用途在于控制資訊的過載,在將迪米特法則運用到系統設計中時,要注意下面的幾點:
- 在類的劃分上,應當盡量創建松耦合的類,類之間的耦合度越低﹐就越有利于復用,一個處在松耦合中的類一旦被修改,不會對關聯的類造成太大波及,
- 在類的結構設計上,每一個類都應當盡量降低其成員變數和成員函式的訪問權限,
- 在類的設計上,只要有可能,一個型別應當設計成不變類,
- 在對其他類的參考上,一個物件對其他物件的參考應當降到最低,
迪米特法則實體
- 下面通過一個簡單實體來加深對迪米特法則的理解,
實體說明
- 某系統界面類(如Form1 ,Form2等類)與資料訪問類(如DAO1,DAO2等類)之間的呼叫關系較為復雜,如下圖所示,

- 由于存在復雜的呼叫關系,將導致系統的耦合度非常大,重用現有類比較困難,增加新的界面類或資料訪問類也比較麻煩,現需要降低界面類和業務邏輯類之間的耦合度,可使用迪米特法則對系統進行重構,
實體決議
- 為了降低界面類與資料訪問類之間的耦合度,可以在它們之間引人一系列控制類(如Controller1,Controller2等類),由控制類來負責控制界面類對業務邏輯類的訪問,重構之后的類圖如下圖所示,

- 在重構過的上圖中,由于控制類的引入,界面類與資料訪問類之間不存在直接參考關系,如果增加一個新的界面類如Form6,需要參考DAO2,DAO3和DAO4,原來需要建立三個參考關系,而有了控制類后,只需要直接參考控制類Controller2即可,如果需要增加新的資料訪問類,可以對應增加新的控制類或者修改現有控制類,無須修改原有界面類,系統具有較好的靈活性,且可以很方便地重用現有的界面類和資料訪問類,
總結
- 對于面向物件的軟體系統設計來說,在支持可維護性的同時,需要提高系統的可復用性,
- 軟體的復用可以提高軟體的開發效率,提高軟體質量,節約開發成本,恰當的復用還可以改善系統的可維護性,
- 單一職責原則要求在軟體系統中,一個類只負責一個功能領域中的相應職責,
- 開閉原則要求一個軟體物體應當對擴展開放,對修改關閉,即在不修改源代碼的基礎上擴展一個系統的行為,
- 里氏替換原則可以通俗表述為在軟體中如果能夠使用基類物件,那么一定能夠使用其子類物件,
- 依賴倒轉原則要求抽象不應該依賴于細節,細節應該依賴于抽象,要針對介面編程,不要針對實作編程,
- 介面隔離原則要求客戶端不應該依賴那些它不需要的介面,即將一些大的介面細化成一些小的介面供客戶端使用,
- 合成復用原則要求復用時盡量使用物件組合,而不使用繼承,
- 迪米特法則要求一個軟體物體應當盡可能少地與其他物體發生相互作用,
- 注意:本文畫圖工具采用 ProcessOn 鏈接:https://www.processon.com/diagrams

這也不是廣告,給大家推薦的工具罷了,本文中的圖片純手繪如有錯別字請見諒,評論區指出我會及時更改的,

- 好了,到此Java面向物件設計原則就總結完畢了,大家看完之后別忘了一鍵三連關注下方公眾號哦 ?
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/301748.html
標籤:java
