設(shè)計(jì)模式
參考資料
大話(huà)設(shè)計(jì)模式
設(shè)計(jì)模式之禪
github我見(jiàn)過(guò)最好的設(shè)計(jì)模式
http://c.biancheng.net/view/1326.html
基本原則
開(kāi)閉原則
在設(shè)計(jì)的時(shí)候盡可能的考慮,需求的變化,新需求來(lái)了盡可能少的改動(dòng)代碼,擁抱變化
定義:指的是軟件中一個(gè)實(shí)體,如類(lèi)、模塊和函數(shù)應(yīng)該對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉。
面向抽象編程
開(kāi)閉是對(duì)擴(kuò)展和修改的約束
強(qiáng)調(diào):用抽象構(gòu)建框架,用實(shí)現(xiàn)擴(kuò)展細(xì)節(jié)。
優(yōu)點(diǎn):提高軟件系統(tǒng)的可復(fù)用性及可維護(hù)性
- 面向?qū)ο笞罨A(chǔ)的設(shè)計(jì)原則
- 指導(dǎo)我們構(gòu)建穩(wěn)定的系統(tǒng)
- 代碼不是一次性的,更多時(shí)間在維護(hù)
- 大多是代碼版本的更新迭代
- 我們最好對(duì)已有的源碼很少修改
- 一般都是新增擴(kuò)展,類(lèi)來(lái)修改
- 能夠降低風(fēng)險(xiǎn)
關(guān)于變化
- 邏輯變化
- 比如說(shuō)算法從
a*b*c變化成a*b+c其實(shí)是可以直接修改的,前提是所有依賴(lài)或者關(guān)聯(lián)類(lèi)都按照相同的邏輯來(lái)處理
- 比如說(shuō)算法從
- 子模塊變化
- 子模塊變化可能直接引起整體也就是高層的變化
- 可見(jiàn)視圖變化
- 如果說(shuō)需求上多了一些原有邏輯不存在的,可能這種變化是恐怖的,需要我們靈活的設(shè)計(jì)
例子
- 彈性工作時(shí)間,時(shí)間是固定的,上下班是可變的
頂層接口
接口是規(guī)范,抽象是實(shí)現(xiàn)

通過(guò)繼承來(lái)解決
價(jià)格的含義已經(jīng)變化了,所以不能夠子類(lèi)直接繼承getPrice(),因?yàn)楫?dāng)前已經(jīng)是折扣價(jià)格了,可能需要價(jià)格和折扣價(jià)格
問(wèn)題
為什么要遵循開(kāi)閉原則,從軟件工程角度怎么理解這點(diǎn)。
- 開(kāi)閉原則對(duì)擴(kuò)展開(kāi)放對(duì)修改關(guān)閉,程序和需求一定是不斷修改的,我們需要把共性和基礎(chǔ)的東西抽出來(lái),把常常修改的東西讓他能夠擴(kuò)展出去,這樣我們程序后期維護(hù)的風(fēng)險(xiǎn)就會(huì)小很多
為什么重要
- 對(duì)于測(cè)試的影響
- 一處變更可能導(dǎo)致原有測(cè)試用例都不管用了
- 提高復(fù)用性
- 高內(nèi)聚,低耦合
- 提高可維護(hù)性
- 面向?qū)ο箝_(kāi)發(fā)的要求
如何使用
- 抽象約束
- 參數(shù)抽到配置中
- 例如sql的連接信息
- 國(guó)際化信息
- 指定項(xiàng)目章程
- 約定項(xiàng)目中Bean都是用自動(dòng)注入,通過(guò)注解來(lái)做裝配
- 團(tuán)隊(duì)成員達(dá)成一致
- 公共類(lèi)走統(tǒng)一的入口,大家都是用統(tǒng)一的公共類(lèi)
- 封裝變化
- 提前預(yù)知變化
依賴(lài)倒置原則
定義
高層模塊不應(yīng)該依賴(lài)低層模塊,二者都應(yīng)該依賴(lài)其抽象。抽象不應(yīng)該依賴(lài)細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴(lài)抽象。
說(shuō)白了就是針對(duì)接口編程,不要針對(duì)實(shí)現(xiàn)編程
什么是倒置
- 不可分割的原子邏輯是底層模塊,原子邏輯在組裝就是高層模塊
- 抽象就是接口或者抽象類(lèi)
- 都不能被實(shí)例化的
- 細(xì)節(jié)
- 細(xì)節(jié)就是具體實(shí)現(xiàn)類(lèi)
優(yōu)點(diǎn)
- 通過(guò)依賴(lài)倒置,能夠減少類(lèi)和類(lèi)之間的耦合性,提高系統(tǒng)的穩(wěn)定性,提高代碼的可讀性和穩(wěn)定性。降低修改程序的風(fēng)險(xiǎn)
例子

public class DipTest {
public static void main(String[] args) {
//===== V1 ========
// Tom tom = new Tom();
// tom.studyJavaCourse();
// tom.studyPythonCourse();
// tom.studyAICourse();
//===== V2 ========
// Tom tom = new Tom();
// tom.study(new JavaCourse());
// tom.study(new PythonCourse());
//===== V3 ========
// Tom tom = new Tom(new JavaCourse());
// tom.study();
//===== V4 ========
Tom tom = new Tom();
tom.setiCourse(new JavaCourse());
tom.study();
}
}
重點(diǎn)
- 先頂層后細(xì)節(jié)
- 自頂向下來(lái)思考全局不要一開(kāi)始沉浸于細(xì)節(jié)
- 高層不依賴(lài)于低層,關(guān)系應(yīng)該用抽象來(lái)維護(hù)
- 針對(duì)接口編程不要針對(duì)實(shí)現(xiàn)編程
以抽象為基準(zhǔn)比以細(xì)節(jié)為基準(zhǔn)搭建起來(lái)的架構(gòu)要穩(wěn)定得多,因此大家在拿到需求之后, 要面向接口編程,先頂層再細(xì)節(jié)來(lái)設(shè)計(jì)代碼結(jié)構(gòu)。
問(wèn)題
為什么要依賴(lài)抽象,抽象表示我還可以擴(kuò)展還沒(méi)有具體實(shí)現(xiàn),按照自己的話(huà)來(lái)解釋一遍
- 一般軟件中抽象分成兩種,接口和抽象類(lèi),接口是規(guī)范,抽象是模板,我們通過(guò)抽象的方式,也就是使用規(guī)范和模板這樣我們能夠使得上層,也就是調(diào)用層能夠復(fù)用邏輯,而我們底層是能夠快速更改實(shí)現(xiàn)的,例如Spring的依賴(lài)注入,Dubbo的SPI,SpringBoot的SPI都如此
依賴(lài)的常見(jiàn)寫(xiě)法
- 構(gòu)造傳遞依賴(lài)對(duì)象
- setter方法傳遞依賴(lài)對(duì)象
- 接口聲明傳遞對(duì)象
最佳實(shí)踐
- 每個(gè)類(lèi)盡量都有接口或抽象類(lèi),或者抽象類(lèi)和接口兩者都具備這是依賴(lài)倒置的基本要求,接口和抽象類(lèi)都是屬于抽象的,有了抽 象才可能依賴(lài)倒置。
- 變量的表面類(lèi)型盡量是接口或者是抽象類(lèi)
- 很多書(shū)上說(shuō)變量的類(lèi)型一定要是接口或者是抽象類(lèi),這個(gè)有點(diǎn)絕對(duì) 化了,比如一個(gè)工具類(lèi),xxxUtils一般是不需要接口或是抽象類(lèi)的。還 有,如果你要使用類(lèi)的clone方法,就必須使用實(shí)現(xiàn)類(lèi),這個(gè)是JDK提供 的一個(gè)規(guī)范。
- 任何類(lèi)都不應(yīng)該從具體類(lèi)派生
- 如果一個(gè)項(xiàng)目處于開(kāi)發(fā)狀態(tài),確實(shí)不應(yīng)該有從具體類(lèi)派生出子類(lèi)的 情況,但這也不是絕對(duì)的,因?yàn)槿硕际菚?huì)犯錯(cuò)誤的,有時(shí)設(shè)計(jì)缺陷是在 所難免的,因此只要不超過(guò)兩層的繼承都是可以忍受的。特別是負(fù)責(zé)項(xiàng) 目維護(hù)的同志,基本上可以不考慮這個(gè)規(guī)則,為什么?維護(hù)工作基本上 都是進(jìn)行擴(kuò)展開(kāi)發(fā),修復(fù)行為,通過(guò)一個(gè)繼承關(guān)系,覆寫(xiě)一個(gè)方法就可 以修正一個(gè)很大的Bug,何必去繼承最高的基類(lèi)呢?(當(dāng)然這種情況盡 量發(fā)生在不甚了解父類(lèi)或者無(wú)法獲得父類(lèi)代碼的情況下。)
- 盡量不要覆寫(xiě)基類(lèi)的方法
- 如果基類(lèi)是一個(gè)抽象類(lèi),而且這個(gè)方法已經(jīng)實(shí)現(xiàn)了,子類(lèi)盡量不要 覆寫(xiě)。類(lèi)間依賴(lài)的是抽象,覆寫(xiě)了抽象方法,對(duì)依賴(lài)的穩(wěn)定性會(huì)產(chǎn)生一 定的影響。
單一職責(zé)原則
不要存在多余一個(gè)導(dǎo)致類(lèi)變更的原因
- 類(lèi)
- 接口
- 方法
只負(fù)責(zé)一項(xiàng)職責(zé)
如果不是這樣設(shè)計(jì),一個(gè)接口負(fù)責(zé)兩個(gè)職責(zé),一旦需求變更,修改其中一個(gè)職責(zé)的邏輯代碼會(huì)導(dǎo)致另外一個(gè)職責(zé)的功能發(fā)生故障。
案例

用戶(hù)信息案例

上述圖片用戶(hù)的屬性和用戶(hù)的行為并沒(méi)有分開(kāi)
- 下圖把
- 用戶(hù)信息抽成BO(Business Object,業(yè)務(wù)對(duì)象)
- 用戶(hù)行為抽成Biz(Business Logic 業(yè)務(wù)邏輯對(duì)象)

電話(huà)

電話(huà)通話(huà)會(huì)發(fā)生下面四個(gè)過(guò)程
- 撥號(hào)
- 通話(huà)
- 回應(yīng)
- 掛機(jī)
上圖的接口做了兩個(gè)事情
- 協(xié)議管理
- dial 撥號(hào)接通
- hangup 掛機(jī)
- 數(shù)據(jù)傳送
- chat
引起變化的點(diǎn)
- 協(xié)議接通會(huì)引起會(huì)引起變化(連接導(dǎo)致不傳輸數(shù)據(jù))
- 可以有不同的通話(huà)方式
打電話(huà),上網(wǎng)
從上面可以看到包含了兩個(gè)職責(zé),應(yīng)該考慮拆分成兩個(gè)接口

優(yōu)點(diǎn)
- 類(lèi)的復(fù)雜性降低,實(shí)現(xiàn)什么職責(zé)都有清晰明確的定義;
- 可讀性提高,復(fù)雜性降低,那當(dāng)然可讀性提高了;
- 可維護(hù)性提高,可讀性提高,那當(dāng)然更容易維護(hù)了;
- 變更引起的風(fēng)險(xiǎn)降低,變更是必不可少的,如果接口的單一職責(zé) 做得好,一個(gè)接口修改只對(duì)相應(yīng)的實(shí)現(xiàn)類(lèi)有影響,對(duì)其他的接口無(wú)影響,這對(duì)系統(tǒng)的擴(kuò)展性、維護(hù)性都有非常大的幫助。
注意
單一職責(zé)原則提出了一個(gè)編寫(xiě)程序的標(biāo)準(zhǔn),用“職責(zé)”或“變 化原因”來(lái)衡量接口或類(lèi)設(shè)計(jì)得是否優(yōu)良,但是“職責(zé)”和“變化原因”都 是不可度量的,因項(xiàng)目而異,因環(huán)境而異。
This is sometimes hard to see,單一職責(zé)確實(shí)收到很多因素制約
- 工期
- 成本
- 技術(shù)水平
- 硬件情況
- 網(wǎng)絡(luò)情況
- 政府政策
接口隔離原則
- 兩個(gè)類(lèi)之間的依賴(lài)應(yīng)該建立在最小的接口上
- 建立單一接口,
不要建立龐大臃腫的接口 - 盡量細(xì)化接口,接口中的方法盡量少
高內(nèi)聚低耦合
例子

問(wèn)題
為什么要把IAnimal拆分成IFlyAnimal,ISwimAnimal,不拆分會(huì)有什么樣的問(wèn)題
- 一個(gè)類(lèi)所提供的功能應(yīng)該是他所真正具有的,不拆分會(huì)導(dǎo)致他不提供的功能但是強(qiáng)行需要實(shí)現(xiàn),而且會(huì)有臃腫的類(lèi)出現(xiàn)
- 可能適配器模式也是為了解決這個(gè)問(wèn)題吧
最佳實(shí)踐
- 一個(gè)接口只服務(wù)于一個(gè)子模塊或者業(yè)務(wù)邏輯
- 通過(guò)業(yè)務(wù)邏輯壓縮接口中的public方法,接口時(shí)常去回顧,盡量 讓接口達(dá)到“滿(mǎn)身筋骨肉”,而不是“肥嘟嘟”的一大堆方法;
- 已經(jīng)被污染了的接口,盡量去修改,若變更的風(fēng)險(xiǎn)較大,則采用
適配器模式進(jìn)行轉(zhuǎn)化處理; - 了解環(huán)境,拒絕盲從。每個(gè)項(xiàng)目或產(chǎn)品都有特定的環(huán)境因素,別看到大師是這樣做的你就照抄。千萬(wàn)別,環(huán)境不同,接口拆分的標(biāo)準(zhǔn)就不同。深入了解業(yè)務(wù)邏輯,最好的接口設(shè)計(jì)就出自你的手中!
迪米特法則
一個(gè)對(duì)象應(yīng)該對(duì)其他對(duì)象保證最少的了解,也稱(chēng)最少知道原則,如果兩個(gè)類(lèi)不必彼此直接通信,那么這兩個(gè)類(lèi)就不應(yīng)該發(fā)生直接的相互作用,如果其中一個(gè)類(lèi)需要調(diào)用另外一個(gè)類(lèi)的某個(gè)方法的話(huà),可以通過(guò)第三者轉(zhuǎn)發(fā)這個(gè)調(diào)用
能夠降低類(lèi)與類(lèi)之間的耦合
強(qiáng)調(diào)只和朋友交流
出現(xiàn)在成員變量、方法的輸入、輸出參數(shù)中的類(lèi)都可以稱(chēng)之為成員朋友類(lèi), 而出現(xiàn)在方法體內(nèi)部的類(lèi)不屬于朋友類(lèi)。
這里面感覺(jué)有點(diǎn)職責(zé)分開(kāi)的感覺(jué),不同的對(duì)象應(yīng)該關(guān)注不同的內(nèi)容,所做的事情也應(yīng)該是自己所關(guān)心的
例子
teamLeader只關(guān)心結(jié)果,不關(guān)心Course

錯(cuò)誤類(lèi)圖如下

問(wèn)題
如果以后你要寫(xiě)代碼和重構(gòu)代碼你怎么分析怎么重構(gòu)?
- 先分析相應(yīng)代碼的職責(zé)
- 把不同的對(duì)象需要關(guān)心的內(nèi)容抽離出來(lái)
- 每個(gè)對(duì)象應(yīng)該只創(chuàng)建和關(guān)心自己所關(guān)心的部分
- 一定要使用的話(huà)可以通過(guò)三方來(lái)使用
- 合適的使用作用域,不要暴露過(guò)多的公共方法和非靜態(tài)的公共方法
注意
迪米特法則要求類(lèi)“羞澀”一點(diǎn),盡量不要對(duì)外公布太多的 public方法和非靜態(tài)的public變量,盡量?jī)?nèi)斂,多使用private、packageprivate、protected等訪問(wèn)權(quán)限。
在實(shí)際的項(xiàng)目中,需要適度地考慮這個(gè)原則,別為了套用原則而做項(xiàng)目。原則只是供參考,如果 違背了這個(gè)原則,項(xiàng)目也未必會(huì)失敗,這就需要大家在采用原則時(shí)反復(fù) 度量,不遵循是不對(duì)的,嚴(yán)格執(zhí)行就是“過(guò)猶不及”。
序列化引起的坑
- 謹(jǐn)慎使用Serializable
- 在一個(gè)項(xiàng)目中使用 RMI(Remote Method Invocation,遠(yuǎn)程方法調(diào)用)方式傳遞一個(gè) VO(Value Object,值對(duì)象),這個(gè)對(duì)象就必須實(shí)現(xiàn)Serializable接口 (僅僅是一個(gè)標(biāo)志性接口,不需要實(shí)現(xiàn)具體的方法),也就是把需要網(wǎng) 絡(luò)傳輸?shù)膶?duì)象進(jìn)行序列化,否則就會(huì)出現(xiàn)NotSerializableException異 常。突然有一天,客戶(hù)端的VO修改了一個(gè)屬性的訪問(wèn)權(quán)限,從private 變更為public,訪問(wèn)權(quán)限擴(kuò)大了,如果服務(wù)器上沒(méi)有做出相應(yīng)的變更, 就會(huì)報(bào)序列化失敗,就這么簡(jiǎn)單。但是這個(gè)問(wèn)題的產(chǎn)生應(yīng)該屬于項(xiàng)目管 理范疇,一個(gè)類(lèi)或接口在客戶(hù)端已經(jīng)變更了,而服務(wù)器端卻沒(méi)有同步更 新,難道不是項(xiàng)目管理的失職嗎?
遵循的原則
? 如果 一個(gè)方法放在本類(lèi)中,既不增加類(lèi)間關(guān)系,也對(duì)本類(lèi)不產(chǎn)生負(fù)面影響, 那就放置在本類(lèi)中。
里氏替換原則
一個(gè)軟件實(shí)體如果能夠適用一個(gè)父親的話(huà),那么一定適用其子類(lèi),所有引用父親的地方必須能透明的使用其子類(lèi)的對(duì)象,子類(lèi)能夠替換父類(lèi)對(duì)象
- 子類(lèi)可以實(shí)現(xiàn)父類(lèi)的抽象方法,但是不能覆蓋父類(lèi)的非抽象方法
- 子類(lèi)中可以增加自己特有的方法
- 子類(lèi)的方法重載父類(lèi)的方法時(shí),入?yún)⒁雀割?lèi)的方法輸入?yún)?shù)更
寬松 - 子類(lèi)實(shí)現(xiàn)父類(lèi)方法的時(shí)候(重寫(xiě)/重載或?qū)崿F(xiàn)抽象方法),方法的后置條件(方法的輸出,返回)要比父類(lèi)更加
嚴(yán)格或者相等
例子
價(jià)格重寫(xiě)問(wèn)題
價(jià)格不是直接重寫(xiě),而是新寫(xiě)一個(gè)方法
public class JavaDiscountCourse extends JavaCourse {
public JavaDiscountCourse(Integer id, String name, Double price) {
super(id, name, price);
}
public Double getDiscountPrice(){
return super.getPrice() * 0.61;
}
}
長(zhǎng)方形和正方形問(wèn)題

public static void resize(Rectangle rectangle){
while (rectangle.getWidth() >= rectangle.getHeight()){
rectangle.setHeight(rectangle.getHeight() + 1);
System.out.println("Width:" +rectangle.getWidth() +",Height:" + rectangle.getHeight());
}
System.out.println("Resize End,Width:" +rectangle.getWidth() +",Height:" + rectangle.getHeight());
}
public class Square extends Rectangle {
private long length;
//勝率
@Override
public void setHeight(long height) {
setLength(height);
}
}
當(dāng)前設(shè)計(jì)會(huì)出現(xiàn)死循環(huán)
解決辦法
抽象接口
public interface QuadRangle {
long getWidth();
long getHeight();
}
返回共同的length
public class Square implements QuadRangle {
private long length;
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
public long getWidth() {
return length;
}
public long getHeight() {
return length;
}
}
當(dāng)前方式子類(lèi)就能夠隨時(shí)替換父類(lèi)了
問(wèn)題
- 你怎么理解里氏替換原則,為什么要保證使用父類(lèi)的地方可以透明地使用子類(lèi)
- 子類(lèi)必須實(shí)現(xiàn)父類(lèi)中沒(méi)有實(shí)現(xiàn)的方法
- is-a的問(wèn)題
- 如果父類(lèi)的地方替換成子類(lèi)不行的話(huà)程序復(fù)雜性增加,繼承反而帶來(lái)了程序的復(fù)雜度
- 子類(lèi)只能在父類(lèi)的基礎(chǔ)上增加新的方法
- 在具體場(chǎng)景中怎么保證使用父類(lèi)的地方可以透明地使用子類(lèi)
- 父類(lèi)返回多使用具體實(shí)現(xiàn),入?yún)⒍嗍褂贸橄蠡蛘哒f(shuō)頂層接口
- 子類(lèi)可以新增一些自己特有的方法
注意
如果子類(lèi)不能完整地實(shí)現(xiàn)父類(lèi)的方法,或者父類(lèi)的某些方法 在子類(lèi)中已經(jīng)發(fā)生“畸變”,則建議斷開(kāi)父子繼承關(guān)系,采用依賴(lài)、聚 集、組合等關(guān)系代替繼承。
盡量避免子類(lèi)的“個(gè)性”,一旦子 類(lèi)有“個(gè)性”,這個(gè)子類(lèi)和父類(lèi)之間的關(guān)系就很難調(diào)和了,把子類(lèi)當(dāng)做父 類(lèi)使用,子類(lèi)的“個(gè)性”被抹殺——委屈了點(diǎn);把子類(lèi)單獨(dú)作為一個(gè)業(yè)務(wù) 來(lái)使用,則會(huì)讓代碼間的耦合關(guān)系變得撲朔迷離——缺乏類(lèi)替換的標(biāo) 準(zhǔn)。
合成復(fù)用原則
盡可能使用對(duì)象組合 has-a組合 或者是 contains-a聚合而不是通過(guò)繼承來(lái)達(dá)到軟件復(fù)用的目的。
- 繼承是白箱復(fù)用
- 所有細(xì)節(jié)都暴露給了子類(lèi)
- 組合和聚合是黑箱復(fù)用
- 對(duì)象歪的對(duì)象獲取不到細(xì)節(jié)
優(yōu)點(diǎn)

問(wèn)題
為什么要多用組合和聚合少用繼承
- 繼承是侵入性的
- Java只支持單繼承
- 降低了代碼的靈活性,子類(lèi)多了很多約束
- 增強(qiáng)了耦合性,父類(lèi)修改的時(shí)候需要考慮子類(lèi)的修改
- 會(huì)導(dǎo)致關(guān)鍵代碼被修改
總結(jié)
如果你只有一把鐵錘, 那么任何東西看上去都像是釘子。
- 適當(dāng)?shù)膱?chǎng)景使用適當(dāng)?shù)脑O(shè)計(jì)原則
- 需要考慮,人力,成本,時(shí)間,質(zhì)量,不要刻意追求完美
- 需要多思考才能用好工具
我的筆記倉(cāng)庫(kù)地址gitee 快來(lái)給我點(diǎn)個(gè)Star吧