設(shè)計(jì)模式
參考資料
圖解設(shè)計(jì)模式
大話設(shè)計(jì)模式
設(shè)計(jì)模式之禪
github我見過最好的設(shè)計(jì)模式
http://c./view/1326.html
基本原則
開閉原則
在設(shè)計(jì)的時(shí)候盡可能的考慮,需求的變化,新需求來了盡可能少的改動(dòng)代碼,擁抱變化
定義:指的是軟件中一個(gè)實(shí)體,如類、模塊和函數(shù)應(yīng)該對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉 。
面向抽象編程
開閉是對(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ò)展,類來修改
- 能夠降低風(fēng)險(xiǎn)
關(guān)于變化
- 邏輯變化
- 比如說算法從
a*b*c 變化成a*b+c 其實(shí)是可以直接修改的,前提是所有依賴或者關(guān)聯(lián)類都按照相同的邏輯來處理
- 子模塊變化
- 可見視圖變化
- 如果說需求上多了一些原有邏輯不存在的,可能這種變化是恐怖的,需要我們靈活的設(shè)計(jì)
例子
- 彈性工作時(shí)間,時(shí)間是固定的,上下班是可變的
頂層接口
接口是規(guī)范,抽象是實(shí)現(xiàn)
通過繼承來解決
價(jià)格的含義已經(jīng)變化了,所以不能夠子類直接繼承getPrice() ,因?yàn)楫?dāng)前已經(jīng)是折扣價(jià)格了,可能需要價(jià)格和折扣價(jià)格
問題
為什么要遵循開閉原則,從軟件工程角度怎么理解這點(diǎn)。
- 開閉原則對(duì)擴(kuò)展開放對(duì)修改關(guān)閉,程序和需求一定是不斷修改的,我們需要把共性和基礎(chǔ)的東西抽出來,把常常修改的東西讓他能夠擴(kuò)展出去,這樣我們程序后期維護(hù)的風(fēng)險(xiǎn)就會(huì)小很多
為什么重要
- 對(duì)于測試的影響
- 提高復(fù)用性
- 提高可維護(hù)性
- 面向?qū)ο箝_發(fā)的要求
如何使用
- 抽象約束
- 參數(shù)抽到配置中
- 指定項(xiàng)目章程
- 約定項(xiàng)目中Bean都是用自動(dòng)注入,通過注解來做裝配
- 團(tuán)隊(duì)成員達(dá)成一致
- 公共類走統(tǒng)一的入口,大家都是用統(tǒng)一的公共類
- 封裝變化
- 提前預(yù)知變化
依賴倒置原則
定義
高層模塊不應(yīng)該依賴低層模塊,二者都應(yīng)該依賴其抽象。抽象不應(yīng)該依賴細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴抽象。
說白了就是針對(duì)接口編程,不要針對(duì)實(shí)現(xiàn)編程
什么是倒置
- 不可分割的原子邏輯是底層模塊,原子邏輯在組裝就是高層模塊
- 抽象就是接口或者抽象類
- 細(xì)節(jié)
- 細(xì)節(jié)就是具體實(shí)現(xiàn)類
優(yōu)點(diǎn)
- 通過依賴倒置,能夠減少類和類之間的耦合性,提高系統(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é)
- 自頂向下來思考全局不要一開始沉浸于細(xì)節(jié)
- 高層不依賴于低層,關(guān)系應(yīng)該用抽象來維護(hù)
- 針對(duì)接口編程不要針對(duì)實(shí)現(xiàn)編程
以抽象為基準(zhǔn)比以細(xì)節(jié)為基準(zhǔn)搭建起來的架構(gòu)要穩(wěn)定得多,因此大家在拿到需求之后, 要面向接口編程,先頂層再細(xì)節(jié)來設(shè)計(jì)代碼結(jié)構(gòu)。
問題
為什么要依賴抽象,抽象表示我還可以擴(kuò)展還沒有具體實(shí)現(xiàn),按照自己的話來解釋一遍
- 一般軟件中抽象分成兩種,接口和抽象類,接口是規(guī)范,抽象是模板,我們通過抽象的方式,也就是使用規(guī)范和模板這樣我們能夠使得上層,也就是調(diào)用層能夠復(fù)用邏輯,而我們底層是能夠快速更改實(shí)現(xiàn)的,例如Spring的依賴注入,Dubbo的SPI,SpringBoot的SPI都如此
依賴的常見寫法
- 構(gòu)造傳遞依賴對(duì)象
- setter方法傳遞依賴對(duì)象
- 接口聲明傳遞對(duì)象
最佳實(shí)踐
- 每個(gè)類盡量都有接口或抽象類,或者抽象類和接口兩者都具備這是依賴倒置的基本要求,接口和抽象類都是屬于抽象的,有了抽 象才可能依賴倒置。
- 變量的表面類型盡量是接口或者是抽象類
- 很多書上說變量的類型一定要是接口或者是抽象類,這個(gè)有點(diǎn)絕對(duì) 化了,比如一個(gè)工具類,xxxUtils一般是不需要接口或是抽象類的。還 有,如果你要使用類的clone方法,就必須使用實(shí)現(xiàn)類,這個(gè)是JDK提供 的一個(gè)規(guī)范。
- 任何類都不應(yīng)該從具體類派生
- 如果一個(gè)項(xiàng)目處于開發(fā)狀態(tài),確實(shí)不應(yīng)該有從具體類派生出子類的 情況,但這也不是絕對(duì)的,因?yàn)槿硕际菚?huì)犯錯(cuò)誤的,有時(shí)設(shè)計(jì)缺陷是在 所難免的,因此只要不超過兩層的繼承都是可以忍受的。特別是負(fù)責(zé)項(xiàng) 目維護(hù)的同志,基本上可以不考慮這個(gè)規(guī)則,為什么?維護(hù)工作基本上 都是進(jìn)行擴(kuò)展開發(fā),修復(fù)行為,通過一個(gè)繼承關(guān)系,覆寫一個(gè)方法就可 以修正一個(gè)很大的Bug,何必去繼承最高的基類呢?(當(dāng)然這種情況盡 量發(fā)生在不甚了解父類或者無法獲得父類代碼的情況下。)
- 盡量不要覆寫基類的方法
- 如果基類是一個(gè)抽象類,而且這個(gè)方法已經(jīng)實(shí)現(xiàn)了,子類盡量不要 覆寫。類間依賴的是抽象,覆寫了抽象方法,對(duì)依賴的穩(wěn)定性會(huì)產(chǎn)生一 定的影響。
單一職責(zé)原則
不要存在多余一個(gè)導(dǎo)致類變更的原因
只負(fù)責(zé)一項(xiàng)職責(zé)
如果不是這樣設(shè)計(jì),一個(gè)接口負(fù)責(zé)兩個(gè)職責(zé),一旦需求變更,修改其中一個(gè)職責(zé)的邏輯代碼會(huì)導(dǎo)致另外一個(gè)職責(zé)的功能發(fā)生故障。
案例
用戶信息案例
上述圖片用戶的屬性和用戶的行為并沒有分開
- 下圖把
- 用戶信息抽成BO(Business Object,業(yè)務(wù)對(duì)象)
- 用戶行為抽成Biz(Business Logic 業(yè)務(wù)邏輯對(duì)象)
電話
電話通話會(huì)發(fā)生下面四個(gè)過程
- 撥號(hào)
- 通話
- 回應(yīng)
- 掛機(jī)
上圖的接口做了兩個(gè)事情
- 協(xié)議管理
- dial 撥號(hào)接通
- hangup 掛機(jī)
- 數(shù)據(jù)傳送
引起變化的點(diǎn)
- 協(xié)議接通會(huì)引起會(huì)引起變化(連接導(dǎo)致不傳輸數(shù)據(jù))
- 可以有不同的通話方式
打電話 ,上網(wǎng)
從上面可以看到包含了兩個(gè)職責(zé),應(yīng)該考慮拆分成兩個(gè)接口
優(yōu)點(diǎn)
- 類的復(fù)雜性降低,實(shí)現(xiàn)什么職責(zé)都有清晰明確的定義;
- 可讀性提高,復(fù)雜性降低,那當(dāng)然可讀性提高了;
- 可維護(hù)性提高,可讀性提高,那當(dāng)然更容易維護(hù)了;
- 變更引起的風(fēng)險(xiǎn)降低,變更是必不可少的,如果接口的單一職責(zé) 做得好,一個(gè)接口修改只對(duì)相應(yīng)的實(shí)現(xiàn)類有影響,對(duì)其他的接口無影響,這對(duì)系統(tǒng)的擴(kuò)展性、維護(hù)性都有非常大的幫助。
注意
單一職責(zé)原則提出了一個(gè)編寫程序的標(biāo)準(zhǔn),用“職責(zé)”或“變 化原因”來衡量接口或類設(shè)計(jì)得是否優(yōu)良,但是“職責(zé)”和“變化原因”都 是不可度量的,因項(xiàng)目而異,因環(huán)境而異。
This is sometimes hard to see ,單一職責(zé)確實(shí)收到很多因素制約
- 工期
- 成本
- 技術(shù)水平
- 硬件情況
- 網(wǎng)絡(luò)情況
- 政府政策
接口隔離原則
- 兩個(gè)類之間的依賴應(yīng)該建立在最小的接口上
- 建立單一接口,
不要建立龐大臃腫的接口
- 盡量細(xì)化接口,接口中的方法盡量少
高內(nèi)聚低耦合
例子
問題
為什么要把IAnimal拆分成IFlyAnimal,ISwimAnimal,不拆分會(huì)有什么樣的問題
- 一個(gè)類所提供的功能應(yīng)該是他所真正具有的,不拆分會(huì)導(dǎo)致他不提供的功能但是強(qiáng)行需要實(shí)現(xiàn),而且會(huì)有臃腫的類出現(xiàn)
- 可能適配器模式也是為了解決這個(gè)問題吧
最佳實(shí)踐
- 一個(gè)接口只服務(wù)于一個(gè)子模塊或者業(yè)務(wù)邏輯
- 通過業(yè)務(wù)邏輯壓縮接口中的public方法,接口時(shí)常去回顧,盡量 讓接口達(dá)到“滿身筋骨肉”,而不是“肥嘟嘟”的一大堆方法;
- 已經(jīng)被污染了的接口,盡量去修改,若變更的風(fēng)險(xiǎn)較大,則采用
適配器模式 進(jìn)行轉(zhuǎn)化處理;
- 了解環(huán)境,拒絕盲從。每個(gè)項(xiàng)目或產(chǎn)品都有特定的環(huán)境因素,別看到大師是這樣做的你就照抄。千萬別,環(huán)境不同,接口拆分的標(biāo)準(zhǔn)就不同。深入了解業(yè)務(wù)邏輯,最好的接口設(shè)計(jì)就出自你的手中!
迪米特法則
一個(gè)對(duì)象應(yīng)該對(duì)其他對(duì)象保證最少的了解,也稱最少知道原則 ,如果兩個(gè)類不必彼此直接通信,那么這兩個(gè)類就不應(yīng)該發(fā)生直接的相互作用,如果其中一個(gè)類需要調(diào)用另外一個(gè)類的某個(gè)方法的話,可以通過第三者轉(zhuǎn)發(fā)這個(gè)調(diào)用
能夠降低類與類之間的耦合
這里面感覺有點(diǎn)職責(zé)分開的感覺,不同的對(duì)象應(yīng)該關(guān)注不同的內(nèi)容,所做的事情也應(yīng)該是自己所關(guān)心的
例子
teamLeader只關(guān)心結(jié)果,不關(guān)心Course
錯(cuò)誤類圖如下
問題
如果以后你要寫代碼和重構(gòu)代碼你怎么分析怎么重構(gòu)?
- 先分析相應(yīng)代碼的職責(zé)
- 把不同的對(duì)象需要關(guān)心的內(nèi)容抽離出來
- 每個(gè)對(duì)象應(yīng)該只創(chuàng)建和關(guān)心自己所關(guān)心的部分
- 一定要使用的話可以通過三方來使用
- 合適的使用作用域,不要暴露過多的公共方法和非靜態(tài)的公共方法
注意
迪米特法則要求類“羞澀”一點(diǎn),盡量不要對(duì)外公布太多的 public方法和非靜態(tài)的public變量,盡量內(nèi)斂,多使用private、packageprivate、protected等訪問權(quán)限。
在實(shí)際的項(xiàng)目中,需要適度地考慮這個(gè)原則,別為了套用原則而做項(xiàng)目。原則只是供參考,如果 違背了這個(gè)原則,項(xiàng)目也未必會(huì)失敗,這就需要大家在采用原則時(shí)反復(fù) 度量,不遵循是不對(duì)的,嚴(yán)格執(zhí)行就是“過猶不及”。
序列化引起的坑
- 謹(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異 常。突然有一天,客戶端的VO修改了一個(gè)屬性的訪問權(quán)限,從private 變更為public,訪問權(quán)限擴(kuò)大了,如果服務(wù)器上沒有做出相應(yīng)的變更, 就會(huì)報(bào)序列化失敗,就這么簡單。但是這個(gè)問題的產(chǎn)生應(yīng)該屬于項(xiàng)目管 理范疇,一個(gè)類或接口在客戶端已經(jīng)變更了,而服務(wù)器端卻沒有同步更 新,難道不是項(xiàng)目管理的失職嗎?
遵循的原則
?如果 一個(gè)方法放在本類中,既不增加類間關(guān)系,也對(duì)本類不產(chǎn)生負(fù)面影響, 那就放置在本類中。
里氏替換原則
一個(gè)軟件實(shí)體如果能夠適用一個(gè)父親的話,那么一定適用其子類,所有引用父親的地方必須能透明的使用其子類的對(duì)象,子類能夠替換父類對(duì)象
- 子類可以實(shí)現(xiàn)父類的抽象方法,但是不能覆蓋父類的非抽象方法
- 子類中可以增加自己特有的方法
- 子類的方法重載父類的方法時(shí),入?yún)⒁雀割惖姆椒ㄝ斎雲(yún)?shù)更
寬松
- 子類實(shí)現(xiàn)父類方法的時(shí)候(重寫/重載或?qū)崿F(xiàn)抽象方法),方法的后置條件(方法的輸出,返回)要比父類更加
嚴(yán)格或者相等
例子
價(jià)格重寫問題
價(jià)格不是直接重寫,而是新寫一個(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;
}
}
長方形和正方形問題
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)前方式子類就能夠隨時(shí)替換父類了
問題
- 你怎么理解里氏替換原則,為什么要保證使用父類的地方可以透明地使用子類
- 子類必須實(shí)現(xiàn)父類中沒有實(shí)現(xiàn)的方法
- is-a的問題
- 如果父類的地方替換成子類不行的話程序復(fù)雜性增加,繼承反而帶來了程序的復(fù)雜度
- 子類只能在父類的基礎(chǔ)上增加新的方法
- 在具體場景中怎么保證使用父類的地方可以透明地使用子類
- 父類返回多使用具體實(shí)現(xiàn),入?yún)⒍嗍褂贸橄蠡蛘哒f頂層接口
- 子類可以新增一些自己特有的方法
注意
如果子類不能完整地實(shí)現(xiàn)父類的方法,或者父類的某些方法 在子類中已經(jīng)發(fā)生“畸變”,則建議斷開父子繼承關(guān)系,采用依賴、聚 集、組合等關(guān)系代替繼承。
盡量避免子類的“個(gè)性”,一旦子 類有“個(gè)性”,這個(gè)子類和父類之間的關(guān)系就很難調(diào)和了,把子類當(dāng)做父 類使用,子類的“個(gè)性”被抹殺——委屈了點(diǎn);把子類單獨(dú)作為一個(gè)業(yè)務(wù) 來使用,則會(huì)讓代碼間的耦合關(guān)系變得撲朔迷離——缺乏類替換的標(biāo) 準(zhǔn)。
合成復(fù)用原則
盡可能使用對(duì)象組合 has-a組合 或者是 contains-a聚合而不是通過繼承來達(dá)到軟件復(fù)用的目的。
- 繼承是白箱復(fù)用
- 組合和聚合是黑箱復(fù)用
- 對(duì)象歪的對(duì)象獲取不到細(xì)節(jié)
優(yōu)點(diǎn)
問題
為什么要多用組合和聚合少用繼承
- 繼承是侵入性的
- Java只支持單繼承
- 降低了代碼的靈活性,子類多了很多約束
- 增強(qiáng)了耦合性,父類修改的時(shí)候需要考慮子類的修改
- 會(huì)導(dǎo)致關(guān)鍵代碼被修改
總結(jié)
如果你只有一把鐵錘, 那么任何東西看上去都像是釘子。
- 適當(dāng)?shù)膱鼍笆褂眠m當(dāng)?shù)脑O(shè)計(jì)原則
- 需要考慮,人力,成本,時(shí)間,質(zhì)量,不要刻意追求完美
- 需要多思考才能用好工具
我的筆記倉庫地址gitee 快來給我點(diǎn)個(gè)Star吧
|