UML類圖
- 用于描述系統(tǒng)中的類(對象)本身的組成和類(對象)之間的各種靜態(tài)關(guān)系。
- 類之間的關(guān)系: 依賴、泛化(繼承)、實(shí)現(xiàn)、關(guān)聯(lián)、聚合與組合
貓是生活中很常見的,我們就拿貓來做個類圖示例:

類圖關(guān)系
面向?qū)ο笫菑?fù)合人類對現(xiàn)實(shí)世界的思維模式,利用面向?qū)ο笤O(shè)計,特別是采用各種設(shè)計模式來解決問題時,會設(shè)計多個類,然后創(chuàng)建多個對象,一個設(shè)計良好的類,應(yīng)該兼顧信息和行為并且高內(nèi)聚,而不同類之間,應(yīng)該做到低耦合。
- 設(shè)計一個類中的信息和行為要高內(nèi)聚
- 設(shè)計多個類,類之間要低耦合
當(dāng)面對應(yīng)用系統(tǒng)或者需要解決的問題經(jīng)常是復(fù)雜的、高度抽象的,我們創(chuàng)建的多個對象往往是有聯(lián)系的,通過對象之間的關(guān)系可以分為以下幾類:
- 泛化(繼承)關(guān)系
- 實(shí)現(xiàn)關(guān)系
- 依賴關(guān)系
- 關(guān)聯(lián)關(guān)系
- 聚合關(guān)系
- 組合關(guān)系
對于泛化(繼承)、實(shí)現(xiàn)這兩種關(guān)系比較簡單,他們體現(xiàn)的是一種類與類、或者類與接口之間的縱向關(guān)系。其他四種關(guān)系則體現(xiàn)的是類與類、類與接口之間的引用/橫向關(guān)系。
這六種關(guān)系所表現(xiàn)的強(qiáng)弱程度,從強(qiáng)到弱依次為:泛化(繼承)= 實(shí)現(xiàn)>組合>聚合>關(guān)聯(lián)>依賴。

1. 泛化關(guān)系(generalization)

- 泛化關(guān)系就是繼承關(guān)系:指的是一個類(子類、子接口)繼承(extends)另一個類(父類、父接口)的功能,并可以增加自己額外的一些功能,繼承是類與類或者接口與接口之間最常見的關(guān)系。
- 在UML類圖中,繼承通常使用 空心三角+實(shí)線 表示,箭頭從子類指向父類。
1912317684-5df8c9c8875e6_articlex.png
2. 實(shí)現(xiàn)關(guān)系(realization)

- 實(shí)現(xiàn)關(guān)系:指的是一個class 類實(shí)現(xiàn)interface接口(可以實(shí)現(xiàn)多個接口)的功能;實(shí)現(xiàn)是類與接口之間最常見的關(guān)系。
- 在UML類圖中,繼承通常使用空心三角+虛線表示,箭頭從實(shí)現(xiàn)類指向接口。
3587295602-5df8c9c7ef901_articlex.png
3.依賴關(guān)系(dependent)

- 依賴關(guān)系:指的是類與類之間的聯(lián)接,依賴關(guān)系表示一個類依賴另一個類的定義。一般而言,依賴關(guān)系再java語言中體現(xiàn)為成員變量、局部變量、方法的形參、方法的返回值,或者對靜態(tài)方法的調(diào)用。
- 依賴是類之間最基礎(chǔ)的、也是最微弱的關(guān)系類型。 如果修改 一個類的定義可能會造成另一個類的變化, 那么這兩個類之 間就存在依賴關(guān)系。 當(dāng)你在代碼中使用具體類的名稱時, 通常意味著存在依賴關(guān)系。 例如在指定方法簽名類型時, 或是通過調(diào)用構(gòu)造函數(shù)對對象進(jìn)行初始化時等。 通過讓代碼依賴接口或抽象類(而不是具體類), 你可以降低其依賴程度。
- 通常情況下, UML 圖不會展示所有依賴——它們在真實(shí)代碼 中的數(shù)量太多了。 為了不讓依賴關(guān)系破壞 UML 圖, 你必須 對其進(jìn)行精心選擇, 僅展示那些對于溝通你的想法來說重要 的依賴關(guān)系。
- 在UML類圖中,依賴關(guān)系用虛線箭頭表示,箭頭從使用類指向被依賴的類。
878185764-5df8c9c4ad6a8_articlex.png
4. 關(guān)聯(lián)關(guān)系(association)

- 關(guān)聯(lián)關(guān)系:指的是類與類之間的聯(lián)接,它使一個類知道另一個類的屬性和方法(實(shí)例變量體現(xiàn))。A類以來與B類,并且把B類作為A類的一個成員變量,則A和B存在關(guān)聯(lián)關(guān)系。
- 關(guān)聯(lián)可以是雙向的,也可以是單向的。兩個類之前是一個層次的,不存在部分跟整體之間的關(guān)系。
- 在UML類圖中,單向關(guān)聯(lián)用實(shí)線箭頭表示,箭頭從使用類指向被關(guān)聯(lián)的類,雙向關(guān)聯(lián)用帶箭頭或者沒有箭頭的實(shí)線來表示。
3411305546-5df8c9c5300eb_articlex.png
為了鞏固對關(guān)聯(lián)和依賴的之間區(qū)別的理解,下面我們來看一個兩者結(jié)合的示例。假設(shè)有個一個Teacher(老師)類:
class Teacher{
val student = Student()
//···
fun teach(c:Course){
//···
this.student.remember(c.getKnowledge())
}
}
teach()(教授知識)方法接收一個來自Course(課程)類的參數(shù)。如果有人修改了Course類的getKnowledge()(獲取知識)方法(修改方法名或添加一些必須得參數(shù)等),代碼將崩潰。這就是依賴關(guān)系。
再來看一下student(學(xué)生)這個成員變量。我們可以肯定Student(學(xué)生)類是Teacher(老師)類的依賴:如果remember()(記?。┓椒ū恍薷模?code>Teacher的代碼也將崩潰。但由于Teacher的所有方法總能訪問student成員變量,所以Student類就不僅是依賴,而也是關(guān)聯(lián)。
5.聚合關(guān)系(aggregation)

- 聚合關(guān)系是關(guān)聯(lián)關(guān)系的一種特例,他體現(xiàn)的是整體與部分,是一種“弱擁有”的關(guān)系,即has-a的關(guān)系。聚合是整體和個體之間的關(guān)系。例如學(xué)校和老師,汽車和輪子。
- 與關(guān)聯(lián)關(guān)系一樣,聚合關(guān)系也是通過實(shí)例變量實(shí)現(xiàn)。但是關(guān)聯(lián)關(guān)系鎖涉及的兩個類是處在同一層次上的,而在聚合關(guān)系中,兩個類是處在不平等的層次上的,一個代表整體,另一個代表部分。
- 聚合關(guān)系表示整體和個體的關(guān)系,整體和個體可以相互獨(dú)立存在,一定是有兩個模塊分別管理整體和個體。
- 在UML類圖中,聚合通常使用空心菱形+實(shí)線箭頭表示,菱形指向整體。
152212015-5df8c9c63feda_articlex.png
6.組合關(guān)系
- 組合關(guān)系是關(guān)聯(lián)關(guān)系的一種特例,它體現(xiàn)的是一個種contains-a(包含)的關(guān)系,這種關(guān)系比聚合更強(qiáng),也稱為強(qiáng)聚合。
- 它要求普通的聚合關(guān)系中代表整體的對象負(fù)責(zé)代表部分對象的生命周期,組合關(guān)系是不能共享的。代表整體的對象需要負(fù)責(zé)保持部分對象和存活,在一些情況下將負(fù)責(zé)代表部分的對象湮滅掉。代表整體的對象可以將代表部分的對象傳遞給另一個對象,由后者負(fù)責(zé)此對象的生命周期。換言之,代表部分的對象在每一個時刻只能與一個對象發(fā)生組合關(guān)系,由后者排他地負(fù)責(zé)生命周期。部分和整體的生命周期一樣。
- 整體和個體不能獨(dú)立存在,一定是在一個模塊中同時管理整體和個體,生命周期必須相同(級聯(lián))。
- 在UML類圖中,組合通常使用實(shí)心菱形+實(shí)線箭頭表示,菱形指向整體。
2230644830-5df8c9c719862_articlex.png
總結(jié)
- 依賴:對類 B 進(jìn)行修改會影響到類 A 。
- 關(guān)聯(lián):對象 A 知道對象 B。 類 A 依賴于類 B。
- 聚合:對象 A 知道對象 B 且由 B 構(gòu)成。 類 A 依賴于類 B。
- 組合:對象 A 知道對象 B、由 B 構(gòu)成而且管理著 B 的生命周 期。 類 A 依賴于類 B。
- 泛化(繼承): 類 A 繼承類 B 的接口和實(shí)現(xiàn), 但是可以對其進(jìn)行擴(kuò) 展。 對象 A 可被視為對象 B。 類 A 依賴于類 B。
- 實(shí)現(xiàn):類 A 定義的方法由接口 B 聲明。 對象 A 可被視為對象 B。 類 A 依賴于類 B。
設(shè)計原則
- 封裝變化的內(nèi)容(方法、類)
- 面向接口進(jìn)行開發(fā),而不是面向?qū)崿F(xiàn)
- 組合由于繼承
這里就不展開說明,感興趣可自行了解。
SOLID原則
- 單一職責(zé)原則(Single Responsibility Principle)
- 開閉原則(Open/close Principle)
- 里氏替換原則(Liskov Substitution Principle)
- 接口隔離原則(Interceface Segregation Principle)
- 依賴倒置原則(Dependency Inversion Principle)
S:單一職責(zé)原則(SRP)
修改一個類的原因只能有一個。
- 定義:對于一個類而言,應(yīng)該僅有一個引起它變化的原因。其中變化的原因就表示了這個類的職責(zé),它可能是某個特定領(lǐng)域的功能,可能是某個需求的解決方案。
- 這個原則表達(dá)的是不要讓一個類承擔(dān)過多的責(zé)任,一旦有了多個職責(zé),那么它就越容易因?yàn)槟硞€職責(zé)而被更改,這樣的狀態(tài)是不穩(wěn)定的,不經(jīng)意的修改很有可能影響到這個類的其他功能。因此,我們需要將不同的職責(zé)封裝在不同的類中,即將不同的變化原因封裝在不同的類中,不同類之間的變化互不影響。
實(shí)例說明
舉一個具體的例子,有一個雇員類Employee,我們有幾個理由來對這個類進(jìn)行修改,第一個 理由與該類的主要工作(管理雇員數(shù)據(jù))有關(guān)。 但還有另一 個理由:時間表報告的格式可能會隨著時間而改變, 從而使你需要對類中的代碼進(jìn)行修改。所以單一職責(zé)原則認(rèn)為這兩個變化的原因事實(shí)上是兩個分離的功能,它們應(yīng)該分離在不同的類中。

相關(guān)設(shè)計模式
面對違背單一職責(zé)原則的程序代碼,我們可以利用外觀模式,代理模式,橋接模式,適配器模式,命令模式對已有設(shè)計進(jìn)行重構(gòu),實(shí)現(xiàn)多職責(zé)的分離。
小結(jié)
單一職責(zé)原則用于控制類的粒度大小,減少類中不相關(guān)功能的代碼耦合,使得類更加的健壯;另外,單一職責(zé)原則也適用于模塊之間解耦,對于模塊的功能劃分有很大的指導(dǎo)意義。
O:開閉原則(OCP)
對于擴(kuò)展, 類應(yīng)該是“開放”的;對于修改, 類則應(yīng) 是“封閉”的。
- 定義:軟件中的對象(類,模塊,函數(shù)等)應(yīng)該對于擴(kuò)展是開放的,但是對于修改是封閉的。這里的對擴(kuò)展開放表示這添加新的代碼,就可以讓程序行為擴(kuò)展來滿足需求的變化;對修改封閉表示在擴(kuò)展程序行為時不要修改已有的代碼,進(jìn)而避免影響原有的功能。
- 要實(shí)現(xiàn)不改代碼的情況下,仍要去改變系統(tǒng)行為的關(guān)鍵就是抽象和多態(tài),通過接口或者抽象類定義系統(tǒng)的抽象層,再通過具體類來進(jìn)行擴(kuò)展。這樣一來,無須對抽象層進(jìn)行任何改動,只需要增加新的具體類來實(shí)現(xiàn)新的業(yè)務(wù)功能即可,達(dá)到開閉原則的要求。
實(shí)例說明
舉一個具體的例子:你的電子商務(wù)程序中包含一個計算運(yùn)輸費(fèi)用的訂單 Order類, 該類中所有運(yùn)輸方法都以硬編碼的方式實(shí)現(xiàn)。 如果你需 要添加一個新的運(yùn)輸方式, 那就必須承擔(dān)對 訂單 類造成破 壞的可能風(fēng)險來對其進(jìn)行修改。

你可以通過應(yīng)用策略模式來解決這個問題。 首先將運(yùn)輸方法 抽取到擁有同樣接口的不同類中。

當(dāng)需要實(shí)現(xiàn)一個新的運(yùn)輸方式時, 你可以通過擴(kuò) 展 運(yùn)輸方式
Shipping 接口來新建一個類, 無需修改任 何 Order 類的代碼。
相關(guān)設(shè)計模式
面對違背開閉原則的程序代碼,可以用到的設(shè)計模式有很多,比如工廠模式,觀察者模式,模板方法模式,策略模式,組合模式,使用相關(guān)設(shè)計模式的關(guān)鍵點(diǎn)就是識別出最有可能變化和擴(kuò)展的部分,然后構(gòu)造抽象來隔離這些變化。
小結(jié)
有了開閉原則,面向需求的變化就能進(jìn)行快速的調(diào)整實(shí)現(xiàn)功能,這大大提高系統(tǒng)的靈活性,可重用性和可維護(hù)性,但會增加一定的復(fù)雜性。
L:里氏替換原則(LSP)
當(dāng)你擴(kuò)展一個類時, 記住你應(yīng)該要能在不修改客戶端 代碼的情況下將子類的對象作為父類對象進(jìn)行傳遞。
- 定義:在不影響程序正確性的基礎(chǔ)上,所有使用基類的地方都能使用其子類的對象來替換。這里提到的基類和子類說的就是具有繼承關(guān)系的兩類對象,當(dāng)我們傳遞一個子類型對象時,需要保證程序不會改變?nèi)魏卧惖男袨楹蜖顟B(tài),程序能正常運(yùn)作。
實(shí)例說明
為了能理解里式替換原則,這里舉一個經(jīng)典的違反里式替換原則的例子:正方形/長方形問題。

上圖為正方形/長方形問題的類層次結(jié)構(gòu),Square 類繼承了 Rectangle 類,但是 Rectangle 類的寬高可以分別修改,但是 Suqare 類的寬高則必須一同修改。如果 User 類操作 Rectangle 類時,但實(shí)際對象是 Suqare 類型時,就會造成程序的出錯,如下方代碼:
Rectangle r = ...; // 返回具體類型對象
r.setWidth(5);
r.setHeight(2);
assert(r.area() == 10);
當(dāng)返回具體類型對象為 Suqare 類型,面積為 10 的斷言就是失敗,這樣明顯是不符合里式替換原則的。
小結(jié)
要讓程序代碼符合里式替換原則,需要保證子類繼承父類時,除添加新的方法完成新增功能外,盡量不要重寫父類的方法,換句話就是子類可以擴(kuò)展父類的功能,但不能改變父類原有的功能。
另一方面,里式替換原則也是對開閉原則的補(bǔ)充,不僅適用于繼承關(guān)系,還適用于實(shí)現(xiàn)關(guān)系的設(shè)計,常提到的 IS-A 關(guān)系是針對行為方式來說的,如果兩個類的行為方式是不相容,那么就不應(yīng)該使用繼承,更好的方式是提取公共部分的方法來代替繼承。
I:接口隔離原則(ISP)
客戶端不應(yīng)被強(qiáng)迫依賴于其不使用的方法。
定義:客戶端不應(yīng)該依賴那些它不需要的接口??蛻舳藨?yīng)該只依賴它實(shí)際使用的方法,因?yàn)槿绻粋€接口具備了若干個方法,那就意味著它的實(shí)現(xiàn)類都要實(shí)現(xiàn)所有接口方法,從代碼結(jié)構(gòu)上就十分臃腫。
實(shí)例說明
假如你創(chuàng)建了一個程序庫, 它能讓程序方便地與多種云計算 供應(yīng)商進(jìn)行整合。 盡管最初版本僅支持阿里云服務(wù), 但它也 覆蓋了一套完整的云服務(wù)和功能。
假設(shè)所有云服務(wù)供應(yīng)商都與阿里云一樣提供相同種類的功能。 但當(dāng)你著手為其他供應(yīng)商提供支持時, 程序庫中絕大部分的 接口會顯得過于寬泛。 其他云服務(wù)供應(yīng)商沒有提供部分方法 所描述的功能。

盡管你仍然可以去實(shí)現(xiàn)這些方法并放入一些樁代碼, 但這絕 不是優(yōu)良的解決方案。 更好的方法是將接口拆分為多個部分。 能夠?qū)崿F(xiàn)原始接口的類現(xiàn)在只需改為實(shí)現(xiàn)多個精細(xì)的接口即 可。 其他類則可僅實(shí)現(xiàn)對自己有意義的接口。

小結(jié)
基于接口隔離原則,我們需要做的就是減少定義大而全的接口,類所要實(shí)現(xiàn)的接口應(yīng)該分解成多個接口,然后根據(jù)所需要的功能去實(shí)現(xiàn),并且在使用到接口方法的地方,用對應(yīng)的接口類型去聲明,這樣可以解除調(diào)用方與對象非相關(guān)方法的依賴關(guān)系??偨Y(jié)一下,接口隔離原則主要功能就是控制接口的粒度大小,防止暴露給客戶端無相關(guān)的代碼和方法,保證了接口的高內(nèi)聚,降低與客戶端的耦合。
D:依賴倒置原則(DIP)
- 高層模塊不應(yīng)該依賴低層模塊,應(yīng)該共同依賴抽象;
- 抽象不應(yīng)該依賴細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴抽象。
這里的抽象就是接口和抽象類,而細(xì)節(jié)就是實(shí)現(xiàn)接口或繼承抽象類而產(chǎn)生的類。
實(shí)例說明
如何理解“高層模塊不應(yīng)該依賴低層模塊,應(yīng)該共同依賴抽象”呢?如果高層模塊依賴于低層模塊,那么低層模塊的改動很有可能影響到高層模塊,從而導(dǎo)致高層模塊被迫改動,這樣一來讓高層模塊的重用變得非常困難。

最佳的做法就如上圖一樣,在高層模塊構(gòu)建一個穩(wěn)定的抽象層,并且只依賴這個抽象層;而由底層模塊完成抽象層的實(shí)現(xiàn)細(xì)節(jié)。這樣一來,高層類都通過該抽象接口使用下一層,移除了高層對底層實(shí)現(xiàn)細(xì)節(jié)的依賴。
依賴倒置原則通常和開閉原則共同發(fā)揮作用:你無需修改已 有類就能用不同的業(yè)務(wù)邏輯類擴(kuò)展低層次的類。
小結(jié)
依賴倒置原則可以減少類間的耦合性,提高系統(tǒng)的穩(wěn)定性,降低并行開發(fā)引起的風(fēng)險,提高代碼的可讀性和可維護(hù)性。同時依賴倒置原則也是框架設(shè)計的核心原則,善于創(chuàng)建可重用的框架和富有擴(kuò)展性的代碼,比如 Tomcat 容器的 Servlet 規(guī)范實(shí)現(xiàn),Spring Ioc 容器實(shí)現(xiàn)。
設(shè)計模式目錄






