面向物件 & 設計原則
目錄
一、什麼是面向物件?
面向物件是一種思想,世間萬物都可以看做一個物件。Java 是一個支援併發、基於類和麵向物件的計算機程式語言。面向物件軟體開發具有以下優點:
- 程式碼開發模組化,更易維護和修改。
- 程式碼複用性強。
- 增強程式碼的可靠性和靈活性。
- 增加程式碼的可讀性。
面向物件和麵向過程的區別?
- 面向過程
- 優點:效能比面向物件高,因為類呼叫時需要例項化,開銷比較大,比較消耗資源。比如,微控制器、嵌入式開發、Linux/Unix 等一般採用面向過程開發,效能是最重要的因素。
- 缺點:沒有面向物件易維護、易複用、易擴充套件。
- 面向物件
- 優點:易維護、易複用、易擴充套件,由於面向物件有封裝、繼承、多型性的特性,可以設計出低耦合的系統,使系統更加靈活、更加易於維護。
- 缺點:效能比面向過程低。
二、面向物件的特徵
四點:封裝、繼承、多型、抽象。
封裝
封裝,利用抽象資料型別將資料和基於資料的操作封裝在一起,給物件提供了隱藏內部特性和行為的能力。只保留一些對外的介面使其與外部發生聯絡。使用者無需關心物件內部的細節,但可以通過物件對外提供的介面來訪問該物件。在 Java 當中,有 4 種修飾符: default
、public
、private
和 protected
。每一種修飾符給其他的位於同一個包或者不同包下面物件賦予了不同的訪問許可權。
優點:
- 通過隱藏物件的屬性來保護物件內部的狀態。
- 減少耦合:可以獨立地開發、測試、優化、使用、理解和修改
- 提高了程式碼的可用性和可維護性,因為物件的行為可以被單獨的改變或者是擴充套件。
- 禁止物件之間的不良互動提高模組化。
以下 Person 類封裝 name、gender、age 等屬性,外界只能通過 get() 方法獲取一個 Person 物件的 name 屬性和 gender 屬性,而無法獲取 age 屬性,但是 age 屬性可以供 work() 方法使用。
注意到 gender 屬性使用 int 資料型別進行儲存,封裝使得使用者注意不到這種實現細節。並且在需要修改 gender 屬性使用的資料型別時,也可以在不影響客戶端程式碼的情況下進行。
public class Person {
private String name;
private int gender;
private int age;
public String getName() {
return name;
}
public String getGender() {
return gender == 0 ? "man" : "woman";
}
public void work() {
if (18 <= age && age <= 50) {
System.out.println(name + " is working very hard!");
} else {
System.out.println(name + " can't work any more!");
}
}
}
繼承
繼承,給物件提供了從基類獲取欄位和方法的能力。繼承提供了程式碼的重用行,也可以在不修改類的情況下給現存的類新增新特性。
繼承實現了 IS-A 關係,例如 Cat 和 Animal 就是一種 IS-A 關係,因此 Cat 可以繼承自 Animal,從而獲得 Animal 非 private 的屬性和方法。
繼承應該遵循里氏替換原則,子類物件必須能夠替換掉所有父類物件。
Cat 可以當做 Animal 來使用,也就是說可以使用 Animal 引用 Cat 物件。父類引用指向子類物件稱為 向上轉型 。
Animal animal = new Cat();
多型
多型,是程式語言給不同的底層資料型別做相同的介面展示的一種能力。一個多型型別上的操作,可以應用到其他型別的值上面。
多型分為編譯時多型和執行時多型:
- 編譯時多型主要指方法的過載
- 執行時多型指程式中定義的物件引用所指向的具體型別在執行期間才確定
執行時多型有三個條件:
- 繼承
- 重寫
- 向上轉型
例如:樂器類(Instrument)有兩個子類:Wind 和 Percussion,它們都覆蓋了父類的 play() 方法,並且在 main() 方法中使用父類 Instrument 來引用 Wind 和 Percussion 物件。在 Instrument 引用呼叫 play() 方法時,會執行實際引用物件所在類的 play() 方法,而不是 Instrument 類的方法。
抽象
抽象將一類物件的共同特徵總結出來構造類的過程,包括資料抽象和行為抽象兩方面。抽象只關注物件有哪些屬性和行為,並不關注這些行為的細節是什麼。
Java 支援建立只暴漏介面而不包含方法實現的抽象的類。這種抽象技術的主要目的是把類的行為和實現細節分離開。
三、設計原則
面向物件設計原則概述
對於面向物件軟體系統的設計而言,在支援可維護性的同時,提高系統的可複用性是一個至關重要的問題,如何同時提高一個軟體系統的可維護性和可複用性是面向物件設計需要解決的核心問題之一。在面向物件設計中,可維護性的複用是以設計原則為基礎的。每一個原則都蘊含一些面向物件設計的思想,可以從不同的角度提升一個軟體結構的設計水平。
面向物件設計原則為支援可維護性複用而誕生,這些原則蘊含在很多設計模式中,它們是從許多設計方案中總結出的指導性原則。面向物件設計原則也是我們用於評價一個設計模式的使用效果的重要指標之一,在設計模式的學習中,大家經常會看到諸如“XXX模式符合XXX原則”、“XXX模式違反了XXX原則”這樣的語句。
S.O.L.I.D
簡寫 | 全拼 | 中文翻譯 | 定義 |
---|---|---|---|
SRP | The Single Responsibility Principle | 單一職責原則 | 一個類只負責一個功能領域中的相應職責 |
OCP | The Open Closed Principle | 開閉原則 | 軟體實體應對擴充套件開放,而對修改關閉 |
LSP | The Liskov Substitution Principle | 里氏替換原則 | 所有引用基類物件的地方能夠透明地使用其子類的物件 |
ISP | The Interface Segregation Principle | 介面隔離原則 | 使用多個專門的介面,而不使用單一的總介面 |
DIP | The Dependency Inversion Principle | 依賴倒置原則 | 抽象不應該依賴於細節,細節應該依賴於抽象 |
其他常見原則 | |||
LoD | The Law of Demeter | 迪米特法則 | 一個軟體實體應當儘可能少地與其他實體發生相互作用 |
CRP | The Composite Reuse Principle | 合成複用原則 | 儘量使用物件組合,而不是繼承來達到複用的目的 |
如果一個類承擔的職責過多,就等於把這些職責耦合在了一起,一個職責的變化可能會削弱這個類完成其它職責的能力。
1. 單一職責原則
單一職責原則是最簡單的面向物件設計原則,它用於控制類的粒度大小。單一職責原則定義如下:
單一職責原則(Single Responsibility Principle, SRP):一個類只負責一個功能領域中的相應職責,
或者可以定義為:就一個類而言,應該只有一個引起它變化的原因。
單一職責原則告訴我們:一個類不能太“累”!在軟體系統中,一個類(大到模組,小到方法)承擔的職責越多,它被複用的可能性就越小,而且一個類承擔的職責過多,就相當於將這些職責耦合在一起,當其中一個職責變化時,可能會影響其他職責的運作,因此要將這些職責進行分離,將不同的職責封裝在不同的類中,即將不同的變化原因封裝在不同的類中,如果多個職責總是同時發生改變則可將它們封裝在同一類中 。
單一職責原則是實現高內聚、低耦合的指導方針,它是最簡單但又最難運用的原則,需要設計人員發現類的不同職責並將其分離,而發現類的多重職責需要設計人員具有較強的分析設計能力和相關實踐經驗。
2. 開閉原則:對擴充套件開放,對修改關閉
開閉原則(Open-Closed Principle, OCP):一個軟體實體應當對擴充套件開放,對修改關閉。
即軟體實體應儘量在不修改原有程式碼的情況下進行擴充套件。
擴充套件就是新增新功能的意思,因此該原則要求在新增新功能時不需要修改程式碼。
符合開閉原則最典型的設計模式是裝飾者模式,它可以動態地將責任附加到物件上,而不用去修改類的程式碼。
在開閉原則的定義中,軟體實體可以指一個軟體模組、一個由多個類組成的區域性結構或一個獨立的類。
任何軟體都需要面臨一個很重要的問題,即它們的需求會隨時間的推移而發生變化。當軟體系統需要面對新的需求時,我們應該儘量保證系統的設計框架是穩定的。如果一個軟體設計符合開閉原則,那麼可以非常方便地對系統進行擴充套件,而且在擴充套件時無須修改現有程式碼,使得軟體系統在擁有適應性和靈活性的同時具備較好的穩定性和延續性。隨著軟體規模越來越大,軟體壽命越來越長,軟體維護成本越來越高,設計滿足開閉原則的軟體系統也變得越來越重要。
為了滿足開閉原則,需要對系統進行抽象化設計,抽象化是開閉原則的關鍵。在Java、C#等程式語言中,可以為系統定義一個相對穩定的抽象層,而將不同的實現行為移至具體的實現層中完成。在很多面向物件程式語言中都提供了介面、抽象類等機制,可以通過它們定義系統的抽象層,再通過具體類來進行擴充套件。如果需要修改系統的行為,無須對抽象層進行任何改動,只需要增加新的具體類來實現新的業務功能即可,實現在不修改已有程式碼的基礎上擴充套件系統的功能,達到開閉原則的要求。
例:Sunny軟體公司開發的CRM系統可以顯示各種型別的圖表,如餅狀圖和柱狀圖等,為了支援多種圖表顯示方式,原始設計方案如圖1所示:
在ChartDisplay類的display()方法中存在如下程式碼片段:
...... if (type.equals("pie")) { PieChart chart = new PieChart(); chart.display(); } else if (type.equals("bar")) { BarChart chart = new BarChart(); chart.display(); } ...... // 在該程式碼中,如果需要增加一個新的圖表類,如折線圖LineChart,則需要修改ChartDisplay類的display()方法的原始碼,增加新的判斷邏輯,違反了開閉原則。 //現對該系統進行重構,使之符合開閉原則。
在本例項中,由於在ChartDisplay類的display()方法中針對每一個圖表類程式設計,因此增加新的圖表類不得不修改原始碼。可以通過抽象化的方式對系統進行重構,使之增加新的圖表類時無須修改原始碼,滿足開閉原則。具體做法如下:
- 增加一個抽象圖表類AbstractChart,將各種具體圖表類作為其子類;
- ChartDisplay類針對抽象圖表類進行程式設計,由客戶端來決定使用哪種具體圖表。
在圖中,我們引入了抽象圖表類AbstractChart,且ChartDisplay針對抽象圖表類進行程式設計,並通過setChart()方法由客戶端來設定例項化的具體圖表物件,在ChartDisplay的display()方法中呼叫chart物件的display()方法顯示圖表。如果需要增加一種新的圖表,如折線圖LineChart,只需要將LineChart也作為AbstractChart的子類,在客戶端向ChartDisplay中注入一個LineChart物件即可,無須修改現有類庫的原始碼。
注意:因為xml和properties等格式的配置檔案是純文字檔案,可以直接通過VI編輯器或記事本進行編輯,且無須編譯,因此在軟體開發中,一般不把對配置檔案的修改認為是對系統原始碼的修改。如果一個系統在擴充套件時只涉及到修改配置檔案,而原有的Java程式碼或C#程式碼沒有做任何修改,該系統即可認為是一個符合開閉原則的系統。
3. 里氏替換原則:所有引用父類的地方必須能替換成其子類的物件
里氏代換原則(Liskov Substitution Principle, LSP):所有引用基類(父類)的地方必須能透明地使用其子類的物件。
里氏代換原則告訴我們,在軟體中將一個基類物件替換成它的子類物件,程式將不會產生任何錯誤和異常,反過來則不成立,如果一個軟體實體使用的是一個子類物件的話,那麼它不一定能夠使用基類物件。例如:我喜歡動物,那我一定喜歡狗,因為狗是動物的子類;但是我喜歡狗,不能據此斷定我喜歡動物,因為我並不喜歡老鼠,雖然它也是動物。
例如有兩個類,一個類為BaseClass,另一個是SubClass類,並且SubClass類是BaseClass類的子類,那麼一個方法如果可以接受一個BaseClass型別的基類物件base的話,如:method1(base),那麼它必然可以接受一個BaseClass型別的子類物件sub,method1(sub)能夠正常執行。反過來的代換不成立,如一個方法method2接受BaseClass型別的子類物件sub為引數:method2(sub),那麼一般而言不可以有method2(base),除非是過載方法。
里氏代換原則是實現開閉原則的重要方式之一,由於使用基類物件的地方都可以使用子類物件,因此在程式中儘量使用基類型別來對物件進行定義,而在執行時再確定其子類型別,用子類物件來替換父類物件。
在使用里氏代換原則時需要注意如下幾個問題:
- 子類的所有方法必須在父類中宣告,或子類必須實現父類中宣告的所有方法。根據里氏代換原則,為了保證系統的擴充套件性,在程式中通常使用父類來進行定義,如果一個方法只存在子類中,在父類中不提供相應的宣告,則無法在以父類定義的物件中使用該方法。
- 我們在運用里氏代換原則時,儘量把父類設計為抽象類或者介面,讓子類繼承父類或實現父介面,並實現在父類中宣告的方法,執行時,子類例項替換父類例項,我們可以很方便地擴充套件系統的功能,同時無須修改原有子類的程式碼,增加新的功能可以通過增加一個新的子類來實現。里氏代換原則是開閉原則的具體實現手段之一。
- Java語言中,在編譯階段,Java編譯器會檢查一個程式是否符合里氏代換原則,這是一個與實現無關的、純語法意義上的檢查,但Java編譯器的檢查是有侷限的。
4. 介面隔離原則:使用多個專門的介面,而不使用單一的總介面
介面隔離原則定義如下:
介面隔離原則(Interface Segregation Principle, ISP):使用多個專門的介面,而不使用單一的總介面,即客戶端不應該依賴那些它不需要的介面
因此使用多個專門的介面比使用單一的總介面要好。
根據介面隔離原則,當一個介面太大時,我們需要將它分割成一些更細小的介面,使用該介面的客戶端僅需知道與之相關的方法即可。每一個介面應該承擔一種相對獨立的角色,不幹不該乾的事,該乾的事都要幹。這裡的“介面”往往有兩種不同的含義:一種是指一個型別所具有的方法特徵的集合,僅僅是一種邏輯上的抽象;另外一種是指某種語言具體的“介面”定義,有嚴格的定義和結構,比如Java語言中的interface。對於這兩種不同的含義,ISP的表達方式以及含義都有所不同:
- 當把“介面”理解成一個型別所提供的所有方法特徵的集合的時候,這就是一種邏輯上的概念,介面的劃分將直接帶來型別的劃分。可以把介面理解成角色,一個介面只能代表一個角色,每個角色都有它特定的一個介面,此時,這個原則可以叫做“角色隔離原則”。
- 如果把“介面”理解成狹義的特定語言的介面,那麼ISP表達的意思是指介面僅僅提供客戶端需要的行為,客戶端不需要的行為則隱藏起來,應當為客戶端提供儘可能小的單獨的介面,而不要提供大的總介面。在面向物件程式語言中,實現一個介面就需要實現該介面中定義的所有方法,因此大的總介面使用起來不一定很方便,為了使介面的職責單一,需要將大介面中的方法根據其職責不同分別放在不同的小介面中,以確保每個介面使用起來都較為方便,並都承擔某一單一角色。介面應該儘量細化,同時介面中的方法應該儘量少,每個介面中只包含一個客戶端(如子模組或業務邏輯類)所需的方法即可,這種機制也稱為“定製服務”,即為不同的客戶端提供寬窄不同的介面。
例:Sunny軟體公司開發人員針對某CRM系統的客戶資料顯示模組設計瞭如圖1所示介面,其中方法dataRead()用於從檔案中讀取資料,方法transformToXML()用於將資料轉換成XML格式,方法createChart()用於建立圖表,方法displayChart()用於顯示圖表,方法createReport()用於建立文字報表,方法displayReport()用於顯示文字報表。
在上圖中,由於在介面CustomerDataDisplay中定義了太多方法,即該介面承擔了太多職責,一方面導致該介面的實現類很龐大,在不同的實現類中都不得不實現介面中定義的所有方法,靈活性較差,如果出現大量的空方法,將導致系統中產生大量的無用程式碼,影響程式碼質量;另一方面由於客戶端針對大介面程式設計,將在一定程式上破壞程式的封裝性,客戶端看到了不應該看到的方法,沒有為客戶端定製介面。因此需要將該介面按照介面隔離原則和單一職責原則進行重構,將其中的一些方法封裝在不同的小介面中,確保每一個介面使用起來都較為方便,並都承擔某一單一角色,每個介面中只包含一個客戶端(如模組或類)所需的方法即可。
在使用介面隔離原則時,我們需要注意控制介面的粒度,介面不能太小,如果太小會導致系統中介面氾濫,不利於維護;介面也不能太大,太大的介面將違背介面隔離原則,靈活性較差,使用起來很不方便。一般而言,介面中僅包含為某一類使用者定製的方法即可,不應該強迫客戶依賴於那些它們不用的方法。
5. 依賴倒置原則:高層模組不應該依賴於低層模組,應該依賴於抽象DI
如果說開閉原則是面向物件設計的目標的話,那麼依賴倒轉原則就是面向物件設計的主要實現機制之一,它是系統抽象化的具體實現。
依賴倒轉原則(Dependency Inversion Principle, DIP):抽象不應該依賴於細節,細節應當依賴於抽象。換言之,要針對介面程式設計,而不是針對實現程式設計。
高層模組不應該依賴於低層模組,二者都應該依賴於抽象;
抽象不應該依賴於細節,細節應該依賴於抽象。
高層模組包含一個應用程式中重要的策略選擇和業務模組,如果高層模組依賴於低層模組,那麼低層模組的改動就會直接影響到高層模組,從而迫使高層模組也需要改動。
依賴於抽象意味著:
- 任何變數都不應該持有一個指向具體類的指標或者引用;
- 任何類都不應該從具體類派生;
- 任何方法都不應該覆寫它的任何基類中的已經實現的方法。
依賴倒轉原則要求我們在程式程式碼中傳遞引數時或在關聯關係中,儘量引用層次高的抽象層類,即使用介面和抽象類進行變數型別宣告、引數型別宣告、方法返回型別宣告,以及資料型別的轉換等,而不要用具體類來做這些事情。為了確保該原則的應用,一個具體類應當只實現介面或抽象類中宣告過的方法,而不要給出多餘的方法,否則將無法呼叫到在子類中增加的新方法。
在引入抽象層後,系統將具有很好的靈活性,在程式中儘量使用抽象層進行程式設計,而將具體類寫在配置檔案中,這樣一來,如果系統行為發生變化,只需要對抽象層進行擴充套件,並修改配置檔案,而無須修改原有系統的原始碼,在不修改的情況下來擴充套件系統的功能,滿足開閉原則的要求。
在實現依賴倒轉原則時,我們需要針對抽象層程式設計,而將具體類的物件通過依賴注入(DependencyInjection, DI)的方式注入到其他物件中,依賴注入是指當一個物件要與其他物件發生依賴關係時,通過抽象來注入所依賴的物件。常用的注入方式有三種,分別是:構造注入,設值注入(Setter注入)和介面注入。構造注入是指通過建構函式來傳入具體類的物件,設值注入是指通過Setter方法來傳入具體類的物件,而介面注入是指通過在介面中宣告的業務方法來傳入具體類的物件。這些方法在定義時使用的是抽象型別,在執行時再傳入具體型別的物件,由子類物件來覆蓋父類物件。
例:
Sunny軟體公司開發人員在開發某CRM系統時發現:該系統經常需要將儲存在TXT或Excel檔案中的客戶資訊轉存到資料庫中,因此需要進行資料格式轉換。在客戶資料操作類中將呼叫資料格式轉換類的方法實現格式轉換和資料庫插入操作,初始設計方案結構如圖所示:
在編碼實現如圖所示結構時,Sunny軟體公司開發人員發現該設計方案存在一個非常嚴重的問題,由於每次轉換資料時資料來源不一定相同,因此需要更換資料轉換類,如有時候需要將TXTDataConvertor改為ExcelDataConvertor,此時,需要修改CustomerDAO的原始碼,而且在引入並使用新的資料轉換類時也不得不修改CustomerDAO的原始碼,系統擴充套件性較差,違反了開閉原則,現需要對該方案進行重構。
在本例項中,由於CustomerDAO針對具體資料轉換類程式設計,因此在增加新的資料轉換類或者更換資料轉換類時都不得不修改CustomerDAO的原始碼。我們可以通過引入抽象資料轉換類解決該問題,在引入抽象資料轉換類DataConvertor之後,CustomerDAO針對抽象類DataConvertor程式設計,而將具體資料轉換類名儲存在配置檔案中,符合依賴倒轉原則。根據里氏代換原則,程式執行時,具體資料轉換類物件將替換DataConvertor型別的物件,程式不會出現任何問題。更換具體資料轉換類時無須修改原始碼,只需要修改配置檔案;如果需要增加新的具體資料轉換類,只要將新增資料轉換類作為DataConvertor的子類並修改配置檔案即可,原有程式碼無須做任何修改,滿足開閉原則。重構後的結構如圖所示:
在上述重構過程中,我們使用了開閉原則、里氏代換原則和依賴倒轉原則,在大多數情況下,這三個設計原則會同時出現,開閉原則是目標,里氏代換原則是基礎,依賴倒轉原則是手段,它們相輔相成,相互補充,目標一致,只是分析問題時所站角度不同而已。
其他常見原則
除了上述的經典原則,在實際開發中還有下面這些常見的設計原則。
簡寫 | 全拼 | 中文翻譯 |
---|---|---|
LOD | The Law of Demeter | 迪米特法則 |
CRP | The Composite Reuse Principle | 合成複用原則 |
CCP | The Common Closure Principle | 共同封閉原則 |
SAP | The Stable Abstractions Principle | 穩定抽象原則 |
SDP | The Stable Dependencies Principle | 穩定依賴原則 |
1. 迪米特法則
迪米特法則又叫作最少知識原則(Least Knowledge Principle,簡寫 LKP),就是說一個物件應當對其他物件有儘可能少的瞭解,不和陌生人說話。
迪米特法則(Law of Demeter, LoD):一個軟體實體應當儘可能少地與其他實體發生相互作用。
如果一個系統符合迪米特法則,那麼當其中某一個模組發生修改時,就會盡量少地影響其他模組,擴充套件會相對容易,這是對軟體實體之間通訊的限制,迪米特法則要求限制軟體實體之間通訊的寬度和深度。迪米特法則可降低系統的耦合度,使類與類之間保持鬆散的耦合關係。
迪米特法則還有幾種定義形式,包括:不要和“陌生人”說話、只與你的直接朋友通訊等,
在迪米特法則中,對於一個物件,其朋友包括以下幾類:
- 當前物件本身(this);
- 以引數形式傳入到當前物件方法中的物件;
- 當前物件的成員物件;
- 如果當前物件的成員物件是一個集合,那麼集合中的元素也都是朋友;
- 當前物件所建立的物件。
任何一個物件,如果滿足上面的條件之一,就是當前物件的“朋友”,否則就是“陌生人”。在應用迪米特法則時,一個物件只能與直接朋友發生互動,不要與“陌生人”發生直接互動,這樣做可以降低系統的耦合度,一個物件的改變不會給太多其他物件帶來影響。
迪米特法則要求我們在設計系統時,應該儘量減少物件之間的互動,如果兩個物件之間不必彼此直接通訊,那麼這兩個物件就不應當發生任何直接的相互作用,如果其中的一個物件需要呼叫另一個物件的某一個方法的話,可以通過第三者轉發這個呼叫。簡言之,就是通過引入一個合理的第三者來降低現有物件之間的耦合度。
在將迪米特法則運用到系統設計中時,要注意下面的幾點:在類的劃分上,應當儘量建立鬆耦合的類,類之間的耦合度越低,就越有利於複用,一個處在鬆耦合中的類一旦被修改,不會對關聯的類造成太大波及;在類的結構設計上,每一個類都應當儘量降低其成員變數和成員函式的訪問許可權;在類的設計上,只要有可能,一個型別應當設計成不變類;在對其他類的引用上,一個物件對其他物件的引用應當降到最低。
例:
Sunny軟體公司所開發CRM系統包含很多業務操作視窗,在這些視窗中,某些介面控制元件之間存在複雜的互動關係,一個控制元件事件的觸發將導致多個其他介面控制元件產生響應,例如,當一個按鈕(Button)被單擊時,對應的列表框(List)、組合框(ComboBox)、文字框(TextBox)、文字標籤(Label)等都將發生改變,在初始設計方案中,介面控制元件之間的互動關係可簡化為如圖所示結構:
由於介面控制元件之間的互動關係複雜,導致在該視窗中增加新的介面控制元件時需要修改與之互動的其他控制元件的原始碼,系統擴充套件性較差,也不便於增加和刪除新控制元件。
現使用迪米特對其進行重構。
在本例項中,可以通過引入一個專門用於控制介面控制元件互動的中間類(Mediator)來降低介面控制元件之間的耦合度。引入中間類之後,介面控制元件之間不再發生直接引用,而是將請求先轉發給中間類,再由中間類來完成對其他控制元件的呼叫。當需要增加或刪除新的控制元件時,只需修改中間類即可,無須修改新增控制元件或已有控制元件的原始碼。
2. 合成複用原則:優先使用組合而不是繼承
合成複用原則(Composite Reuse Principle, CRP):儘量使用物件組合,而不是繼承來達到複用的
目的。
合成複用原則就是在一個新的物件裡通過關聯關係(包括組合關係和聚合關係)來使用一些已有的物件,使之成為新物件的一部分;新物件通過委派呼叫已有物件的方法達到複用功能的目的。簡言之:複用時要儘量使用組合/聚合關係(關聯關係),少用繼承。
在面向物件設計中,可以通過兩種方法在不同的環境中複用已有的設計和實現,即通過組合/聚合關係或通過繼承,但首先應該考慮使用組合/聚合,組合/聚合可以使系統更加靈活,降低類與類之間的耦合度,一個類的變化對其他類造成的影響相對較少;其次才考慮繼承,在使用繼承時,需要嚴格遵循里氏代換原則,有效使用繼承會有助於對問題的理解,降低複雜度,而濫用繼承反而會增加系統構建和維護的難度以及系統的複雜度,因此需要慎重使用繼承複用。
通過繼承來進行復用的主要問題在於繼承複用會破壞系統的封裝性,因為繼承會將基類的實現細節暴露給子類,由於基類的內部細節通常對子類來說是可見的,所以這種複用又稱“白箱”複用,如果基類發生改變,那麼子類的實現也不得不發生改變;從基類繼承而來的實現是靜態的,不可能在執行時發生改變,沒有足夠的靈活性;而且繼承只能在有限的環境中使用(如類沒有宣告為不能被繼承)。
由於組合或聚合關係可以將已有的物件(也可稱為成員物件)納入到新物件中,使之成為新物件的一部分,因此新物件可以呼叫已有物件的功能,這樣做可以使得成員物件的內部實現細節對於新物件不可見,所以這種複用又稱為“黑箱”複用,相對繼承關係而言,其耦合度相對較低,成員物件的變化對新物件的影響不大,可以在新物件中根據實際需要有選擇性地呼叫成員物件的操作;合成複用可以在執行時動態進行,新物件可以動態地引用與成員物件型別相同的其他物件。
一般而言,如果兩個類之間是“Has-A”的關係應使用組合或聚合,如果是“Is-A”關係可使用繼承。"Is-A"是嚴格的分類學意義上的定義,意思是一個類是另一個類的"一種";而"Has-A"則不同,它表示某一個角色具有某一項責任。
例:
Sunny軟體公司開發人員在初期的CRM系統設計中,考慮到客戶數量不多,系統採用MySQL作為資料庫,與資料庫操作有關的類如CustomerDAO類等都需要連線資料庫,連線資料庫的方法getConnection()封裝在DBUtil類中,由於需要重用DBUtil類的getConnection()方法,設計人員將CustomerDAO作為DBUtil類的子類,初始設計方案結構如圖所示:
隨著客戶數量的增加,系統決定升級為Oracle資料庫,因此需要增加一個新的OracleDBUtil類來連線Oracle資料庫,由於在初始設計方案中CustomerDAO和DBUtil之間是繼承關係,因此在更換資料庫連線方式時需要修改CustomerDAO類的原始碼,將CustomerDAO作為OracleDBUtil的子類,這將違反開閉原則。【當然也可以修改DBUtil類的原始碼,同樣會違反開閉原則。】
現使用合成複用原則對其進行重構。
CustomerDAO和DBUtil之間的關係由繼承關係變為關聯關係,採用依賴注入的方式將DBUtil物件注入到CustomerDAO中,可以使用構造注入,也可以使用Setter注入。如果需要對DBUtil的功能進行擴充套件,可以通過其子類來實現,如通過子類OracleDBUtil來連線Oracle資料庫。由於CustomerDAO針對DBUtil程式設計,根據里氏代換原則,DBUtil子類的物件可以覆蓋DBUtil物件,只需在CustomerDAO中注入子類物件即可使用子類所擴充套件的方法。例如在CustomerDAO中注入OracleDBUtil物件,即可實現Oracle資料庫連線,原有程式碼無須進行修改,而且還可以很靈活地增加新的資料庫連線方式。
3. 共同封閉原則
一起修改的類,應該組合在一起(同一個包裡)。如果必須修改應用程式裡的程式碼,我們希望所有的修改都發生在一個包裡(修改關閉),而不是遍佈在很多包裡。
4. 穩定抽象原則
最穩定的包應該是最抽象的包,不穩定的包應該是具體的包,即包的抽象程度跟它的穩定性成正比。
5. 穩定依賴原則
包之間的依賴關係都應該是穩定方向依賴的,包要依賴的包要比自己更具有穩定性。
總結
這 7 種設計原則是軟體設計模式必須儘量遵循的原則,各種原則要求的側重點不同。
其中,
【開閉原則】是總綱,它告訴我們要【對擴充套件開放,對修改關閉】;
【里氏替換原則】告訴我們【不要破壞繼承體系】;
【依賴倒置原則】告訴我們要【面向介面程式設計】;
【單一職責原則】告訴我們實現【類】要【職責單一】;
【介面隔離原則】告訴我們在設計【介面】的時候要【精簡單一】;
【迪米特法則】告訴我們要【降低耦合度】;
【合成複用原則】告訴我們要【優先使用組合或者聚合關係複用,少用繼承關係複用】 。