在講設(shè)計(jì)原則之前,我先強(qiáng)制灌輸大家一波雞湯,提倡
面向接口編程,代碼的設(shè)計(jì)更重要的是考慮以后的擴(kuò)展和可維護(hù)性
大家?guī)е@樣的思維來(lái)學(xué)習(xí)設(shè)計(jì)模式以及設(shè)計(jì)原則,慢慢就意會(huì)這波毒雞湯了。
先聲明一點(diǎn)就是老衲的blog,也是邊學(xué)習(xí),邊記錄,而后以討論交流的方式敘述,有什么不對(duì)的地方大家多多擔(dān)待。
創(chuàng)建型-抽象工廠(chǎng)(Abstract Factory)
創(chuàng)建型-工廠(chǎng)方法(Factory Method)
行為型-責(zé)任鏈(Chain of Responsibility)
設(shè)計(jì)原則
單一職責(zé)原則(Single Responsibility Principle, 簡(jiǎn)稱(chēng)是SRP)
老衲是一位粗鄙之人,所以描述也是盡可能的白話(huà)哈~
定義
白話(huà)單一職責(zé):首先,顧名思義,什么是單一職責(zé)?就是某玩意,專(zhuān)門(mén)負(fù)責(zé)某個(gè)東西,就譬如說(shuō),你的手機(jī)屏幕,他就只負(fù)責(zé)顯示,不管顯示app還是視頻還是小黃書(shū),只要是顯示的活,他就干,而且他就只干顯示的活,這就是單一職責(zé),那么放到術(shù)語(yǔ)里面就是咱們?cè)O(shè)計(jì)的接口或者類(lèi),盡量遵循此原則,有什么好處賴(lài)?
- 類(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ù)性都有非常大
的幫助
這一波好處摘自設(shè)計(jì)模式之禪,總結(jié)下來(lái)就是,看見(jiàn)這個(gè)接口所聲明的方法,你就知道功能都有什么,初學(xué)者或者初接手的人都可以很快融入到代碼中進(jìn)行迭代和維護(hù)了。
然而道理是這個(gè)道理,但是具體在設(shè)計(jì)代碼的時(shí)候,還是要考慮到具體的應(yīng)用下。用書(shū)中的話(huà)描述就是
單一職責(zé)原則提出了一個(gè)編寫(xiě)程序的標(biāo)準(zhǔn), 用“職責(zé)”或“變化原因”來(lái)衡量接口或 類(lèi)設(shè)計(jì)得是否優(yōu)良, 但是“職責(zé)”和“變化原因”都是不可度量的, 因項(xiàng)目而異, 因環(huán)境而異
code
OK,結(jié)合上面說(shuō)的小Demo,接下來(lái)來(lái)一杯Java解解渴
interface IScreenDisplay {
/**
* display image on screen
*
* @param image
*/
void displayImage(String image);
/**
* display a text on screen
*
* @param text
*/
void displayText(String text);
/**
* display a video on screen
*
* @param video
*/
void displayVideo(String video);
}
上來(lái)就是我們的小接口,屏幕顯示,干什么玩意呢?自行翻譯不謝~
然后是我們的實(shí)現(xiàn)類(lèi)
static class Phone implements IScreenDisplay {
@Override
public void displayImage(String image) {
System.out.println("displayImage:" + image);
}
@Override
public void displayText(String text) {
System.out.println("displayText:" + text);
}
@Override
public void displayVideo(String video) {
System.out.println("displayVideo:" + video);
}
}
實(shí)現(xiàn)類(lèi)就是干具體的活了,國(guó)際慣例sout輸出~
public static void main(String[] args) {
IScreenDisplay phone = new Phone();
phone.displayImage("ic_launcher.png");
phone.displayText("Hello Done!");
phone.displayVideo("xiao huang pian.avi");
}
這一套降龍十八掌走下來(lái)結(jié)果:
displayImage:ic_launcher.png
displayText:Hello Done!
displayVideo:xiao huang pian.avi
多囂張?多簡(jiǎn)單?當(dāng)后期review一看,咱們的接口告訴你只負(fù)責(zé)顯示,可以顯示文字,圖片和視頻,至于什么時(shí)候顯示,顯示什么內(nèi)容,那我不管,我就顯示,唯一讓我引起變化的是什么?當(dāng)然是內(nèi)容咯~
總結(jié)一番便是
接口一定要做到單一職責(zé), 類(lèi)的設(shè)計(jì)盡量做到只有一個(gè)原因引起變化
里氏替換原則(LiskovSubstitution Principle, LSP)
此原則相較于上面的單一職責(zé),要復(fù)雜一些,這里引用書(shū)中的原話(huà)(一定要認(rèn)真閱讀),后面會(huì)通過(guò)白話(huà)做出相關(guān)解釋哈~
首先要理解的是繼承的特點(diǎn)
- 代碼共享, 減少創(chuàng)建類(lèi)的工作量, 每個(gè)子類(lèi)都擁有父類(lèi)的方法和屬性
- 提高代碼的重用性
- 子類(lèi)可以形似父類(lèi),但又異于父類(lèi), “龍生龍, 鳳生鳳,老鼠生來(lái)會(huì)打洞”是說(shuō)子擁有父的“種”,“世界上沒(méi)有兩片完全相同的葉子”是指明子與父的不同
- 提高代碼的可擴(kuò)展性,實(shí)現(xiàn)父類(lèi)的方法就可以“為所欲為”了,君不見(jiàn)很多開(kāi)源框架的擴(kuò)展接口都是通過(guò)繼承父類(lèi)來(lái)完成的
- 提高產(chǎn)品或項(xiàng)目的開(kāi)放性
上面是優(yōu)點(diǎn),下面是缺點(diǎn)
- 繼承是侵入性的。 只要繼承, 就必須擁有父類(lèi)的所有屬性和方法
- 降低代碼的靈活性。子類(lèi)必須擁有父類(lèi)的屬性和方法,讓子類(lèi)自由的世界中多了些約束
- 增強(qiáng)了耦合性。當(dāng)父類(lèi)的常量、變量和方法被修改時(shí),需要考慮子類(lèi)的修改,而且在缺乏規(guī)范的環(huán)境下,這種修改可能帶來(lái)非常糟糕的結(jié)果——大段的代碼需要重構(gòu)
定義
- 看不懂,繞口定義:
If for each object o1 of type S there is an object o2 oftype T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 issubstituted for o2 then S is a subtype of T.(如果對(duì)每一個(gè)類(lèi)型為S的對(duì)象o1, 都有類(lèi)型為T(mén)的對(duì)象o2, 使得以T定義的所有程序P在所有的對(duì)象o1都代換成o2時(shí), 程序P的行為沒(méi)有發(fā)生變
化, 那么類(lèi)型S是類(lèi)型T的子類(lèi)型。 )
- 通俗易懂,親民定義:
Functions that use pointers or references to base classes must be able to useobjects of derived classes without knowing it.(所有引用基類(lèi)的地方必須能透明地使用其子類(lèi)的對(duì)象。 )
只要父類(lèi)能出現(xiàn)的地方子類(lèi)就可以出現(xiàn), 而且
替換為子類(lèi)也不會(huì)產(chǎn)生任何錯(cuò)誤或異常, 使用者可能根本就不需要知道是父類(lèi)還是子類(lèi)。 但
是, 反過(guò)來(lái)就不行了, 有子類(lèi)出現(xiàn)的地方, 父類(lèi)未必就能適應(yīng)。
里氏替換原則為良好的繼承定義了一個(gè)規(guī)范,一句簡(jiǎn)單的定義包含了4層含義
- 子類(lèi)必須完全實(shí)現(xiàn)父類(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è)性
向下轉(zhuǎn)型(downcast)是不安全的, 從里氏替換原則來(lái)看,就是有子類(lèi)出現(xiàn)的地方父類(lèi)未必就可以出現(xiàn)
- 覆蓋或?qū)崿F(xiàn)父類(lèi)的方法時(shí)輸入?yún)?shù)可以被放大
里氏替換原則也要求制定一個(gè)契約, 就是父類(lèi)或接口,這種設(shè)計(jì)方法也叫做Design by Contract(契約設(shè)計(jì)) ,與里氏替換原則有著異曲同工之妙。 契約制定了, 也就同時(shí)制定了前置條件和后置條件, 前置條件就是你要讓我執(zhí)行,就必須滿(mǎn)足我的條件; 后置條件就是我執(zhí)行完了需要反饋, 標(biāo)準(zhǔn)是什么。
- 覆寫(xiě)或?qū)崿F(xiàn)父類(lèi)的方法時(shí)輸出結(jié)果可以被縮小
采用里氏替換原則的目的就是增強(qiáng)程序的健壯性,版本升級(jí)時(shí)也可以保持非常好的兼容性。即使增加子類(lèi),原有的子類(lèi)還可以繼續(xù)運(yùn)行,每個(gè)子類(lèi)對(duì)應(yīng)不同的業(yè)務(wù)含義,使用父類(lèi)作為參數(shù),傳遞不同的子類(lèi)完成不同的業(yè)務(wù)邏輯
白話(huà)方式總結(jié)一下上面的內(nèi)容:
里氏替換原則更像是一種java繼承的規(guī)范用法,"extends"嘛,大家都用過(guò)的東西,可以這么用那么用隨便用,想用就用,但是寫(xiě)完以后發(fā)現(xiàn)沒(méi)有卵用,用是用了,但是有什么奧妙還是不太清楚,為什么要用它?
咱們經(jīng)常談到的一個(gè)詞:抽取,譬如說(shuō)activity,這個(gè)活動(dòng)要setContentView,那個(gè)活動(dòng)也要setContentView,這個(gè)活動(dòng)要上下文,那個(gè)活動(dòng)也要上下文,誒~這時(shí)候我們就開(kāi)始搞一個(gè)BaseActivity的東西,然后讓所有子類(lèi)去重寫(xiě)獲取布局ID,同時(shí)父類(lèi)直接拿到自己的上下文對(duì)象,子類(lèi)直接使用即可。
那么是不是我們的活動(dòng)程序代碼邏輯無(wú)論ams(自行百度)怎么使,你創(chuàng)建的這個(gè)活動(dòng)都好使,符合咱們android體系的健壯性,另外你想實(shí)現(xiàn)的黑科技是不是都可以在自己的活動(dòng)里面去實(shí)現(xiàn),當(dāng)然這里的設(shè)計(jì)并不是完全符合里氏替換原則,姑且斷章取義,您就這么斷章取義的理解即可,另外如果老衲說(shuō)的有什么不對(duì)的地方,歡迎大家指正批評(píng)~
還記得上面提到過(guò)的Java中繼承帶來(lái)的優(yōu)缺點(diǎn)嗎?咱們的里氏替換原則就是一個(gè)“揚(yáng)長(zhǎng)避短”的做法,具體怎么搞?code一下見(jiàn)分曉
code
還是手機(jī)的例子,這次咱們不說(shuō)屏幕,說(shuō)品牌,先來(lái)個(gè)抽象手機(jī)
abstract class AbsPhone {
/**
* 使用
*/
public abstract void use();
}
臥槽,就那么簡(jiǎn)單,抽象一個(gè)使用的方法~
OK,接下來(lái)是子類(lèi)們
/**
* 諾基亞
*/
class NokiaPhone extends AbsPhone {
@Override
public void use() {
System.out.println("砸核桃 實(shí)用戶(hù)");
}
}
/**
* 錘子
*/
class TPhone extends AbsPhone {
@Override
public void use() {
System.out.println("錘子 情懷戶(hù)");
}
}
/**
* 蘋(píng)果
*/
class ApplePhone extends AbsPhone {
private void ringing() {
System.out.println("先讓蘋(píng)果特有鈴聲響一陣...嗚嗚嗚");
}
@Override
public void use() {
ringing();
System.out.println("蘋(píng)果 zhuang bi 專(zhuān)業(yè)戶(hù)");
}
}
很簡(jiǎn)單,就是諾基亞,錘子和蘋(píng)果三個(gè)街機(jī),這里并沒(méi)有任何對(duì)這些品牌的觀(guān)點(diǎn),這是假借名義,傳輸知識(shí)用
再然后必須有一個(gè)使用者嘛
class Person {
AbsPhone phone;
String name;
public Person(String name) {
this.name = name;
}
public void setPhone(AbsPhone phone) {
this.phone = phone;
}
void communicate() {
System.out.println(name + "掏出手機(jī)了...");
phone.use();
}
}
然后我們來(lái)用這些兄dei跑一把
public static void main(String[] args) {
Person coke = new Person("庫(kù)克");
coke.setPhone(new ApplePhone());
Person laoluo = new Person("老羅");
laoluo.setPhone(new TPhone());
Person bill = new Person("比爾蓋茨");
bill.setPhone(new NokiaPhone());
coke.communicate();
laoluo.communicate();
bill.communicate();
}
sout
庫(kù)克掏出手機(jī)了...
先讓蘋(píng)果特有鈴聲響一陣...嗚嗚嗚
蘋(píng)果 zhuang bi 專(zhuān)業(yè)戶(hù)
老羅掏出手機(jī)了...
錘子 情懷戶(hù)
比爾蓋茨掏出手機(jī)了...
砸核桃 實(shí)用戶(hù)
爽哉,爽在哪里了?咱們的person對(duì)象只知道自己有個(gè)手機(jī),這個(gè)手機(jī)能執(zhí)行communicate操作,什么手機(jī)我不管,我只管用它。術(shù)語(yǔ)就是,邏輯代碼不管實(shí)現(xiàn),只需要持有著抽象類(lèi),然后執(zhí)行抽象類(lèi)提供的方法即可,就算以后擴(kuò)展了其他的子類(lèi),也不影響我之前的業(yè)務(wù)邏輯,子類(lèi)完全繼承了父類(lèi),同時(shí)在不改變方法本身的邏輯下增添了自己的特色,同時(shí)也符合父類(lèi)出現(xiàn)的地方,就可以替換成子類(lèi)
請(qǐng)大家仔細(xì)咀嚼下面這兩段話(huà):
采用里氏替換原則的目的就是增強(qiáng)程序的健壯性, 版本升級(jí)時(shí)也可以保持非常好的兼容性。 即使增加子類(lèi), 原有的子類(lèi)還可以繼續(xù)運(yùn)行。 在實(shí)際項(xiàng)目中, 每個(gè)子類(lèi)對(duì)應(yīng)不同的業(yè)務(wù)含義, 使用父類(lèi)作為參數(shù), 傳遞不同的子類(lèi)完成不同的業(yè)務(wù)邏輯
對(duì)于基類(lèi)中定義的所有子程序,用在它的任何一個(gè)派生類(lèi)中時(shí)的含義都應(yīng)該是相同的。這樣繼承才不會(huì)增加復(fù)雜度,基類(lèi)才能真正被復(fù)用,而派生類(lèi)也能夠在基類(lèi)的基礎(chǔ)上增加新的行為。如果我們必須要不斷地思考不同派生類(lèi)的實(shí)現(xiàn)在語(yǔ)義上的差異,繼承就只會(huì)增加復(fù)雜度了。
本著負(fù)責(zé)人的態(tài)度,老衲還是把樹(shù)立的這段話(huà)copy過(guò)來(lái),望大家酌情使用參考:
在項(xiàng)目中,采用里氏替換原則時(shí),盡量避免子類(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)
依賴(lài)倒置原則(Dependence Inversion Principle,DIP)
依賴(lài)倒置是什么鬼?純說(shuō)感覺(jué)也說(shuō)不明白,白話(huà)點(diǎn)來(lái)說(shuō)就是各種注入,依賴(lài)的接口注入,就是咱們的接口不依賴(lài)實(shí)現(xiàn),而具體的實(shí)現(xiàn)類(lèi)去組裝這些接口,簡(jiǎn)單粗暴的解釋就是,
面向接口編程(OOD)
祭出官方釋義就是:
High level modules should not depend upon low level modules.Both should depend upon
abstractions.Abstractions should not depend upon details.Details should depend upon abstractions
- 高層模塊不應(yīng)該依賴(lài)低層模塊, 兩者都應(yīng)該依賴(lài)其抽象
- 抽象不應(yīng)該依賴(lài)細(xì)節(jié)
- 細(xì)節(jié)應(yīng)該依賴(lài)抽象
定義
抽象就是指接口或抽象類(lèi),兩者都是不能直接被實(shí)例化的;細(xì)節(jié)就是實(shí)現(xiàn)類(lèi),實(shí)現(xiàn)接口或繼承抽象類(lèi)而產(chǎn)生的類(lèi)就是細(xì)節(jié),其特點(diǎn)就是可以直接被實(shí)例化,也就是可以加上一個(gè)關(guān)鍵字new產(chǎn)生一個(gè)對(duì)象
- 模塊間的依賴(lài)通過(guò)抽象發(fā)生, 實(shí)現(xiàn)類(lèi)之間不發(fā)生直接的依賴(lài)關(guān)系, 其依賴(lài)關(guān)系是通過(guò)接口或抽象類(lèi)產(chǎn)生的
- 接口或抽象類(lèi)不依賴(lài)于實(shí)現(xiàn)類(lèi)
- 實(shí)現(xiàn)類(lèi)依賴(lài)接口或抽象類(lèi)
那么采用依賴(lài)倒置的優(yōu)勢(shì)在哪里?就是
減少類(lèi)間的耦合性, 提高系統(tǒng)的穩(wěn)定性,降低并行開(kāi)發(fā)引起的風(fēng)險(xiǎn),提高代碼的可讀性和可維護(hù)性,穩(wěn)定性較高的設(shè)計(jì),在周?chē)h(huán)境頻繁變化的時(shí)候,依然可以做到“我自巋然不動(dòng)”
那么要如何遵循依賴(lài)倒置原則去設(shè)計(jì)代碼呢?首先,我們的始終遵循
- 抽象不依賴(lài)細(xì)節(jié)
- 在新增加低層模塊時(shí),只修改了業(yè)務(wù)場(chǎng)景類(lèi),也就是高層模塊,對(duì)其他低層模塊如Driver類(lèi)不需要做任何修改,業(yè)務(wù)就可以運(yùn)行,把“變更”引起的風(fēng)險(xiǎn)擴(kuò)散降到最低
- 如果兩個(gè)類(lèi)直接存在依賴(lài)關(guān)系,那么連接他們之間的橋梁就是接口,不依賴(lài)具體的低層模塊
- 抽象是對(duì)實(shí)現(xiàn)的約束,對(duì)依賴(lài)者而言,也是一種契約,不僅僅約束自己,還同時(shí)約束自己與外部的關(guān)系,其目的是保證所有的細(xì)節(jié)不脫離契約的范疇,確保約束雙方按照既定的契約(抽象)共同發(fā)展,只要抽象這根基線(xiàn)在,細(xì)節(jié)就脫離不了這個(gè)圈圈,始終讓你的對(duì)象做到“言必信, 行必果”
- 常用依賴(lài)傳遞,只要做到抽象依賴(lài),即使是多層的依賴(lài)傳遞也無(wú)所畏懼
code
依賴(lài)倒置原則的本質(zhì)就是通過(guò)抽象(接口或抽象類(lèi)) 使各個(gè)類(lèi)或模塊的實(shí)現(xiàn)彼此獨(dú)立,
不互相影響, 實(shí)現(xiàn)模塊間的松耦合
- 每個(gè)類(lèi)盡量都有接口或抽象類(lèi), 或者抽象類(lèi)和接口兩者都具備
這是依賴(lài)倒置的基本要求, 接口和抽象類(lèi)都是屬于抽象的, 有了抽象才可能依賴(lài)倒置。 - 變量的表面類(lèi)型盡量是接口或者是抽象類(lèi)
- 任何類(lèi)都不應(yīng)該從具體類(lèi)派生
- 盡量不要覆寫(xiě)基類(lèi)的方法(類(lèi)間依賴(lài)的是抽象, 覆寫(xiě)了抽象方法, 對(duì)依賴(lài)的穩(wěn)定性會(huì)產(chǎn)生一定的影響)
- 結(jié)合里氏替換原則使用(接口負(fù)責(zé)定義public屬性和方法, 并且聲明與其他對(duì)象的依賴(lài)關(guān)系,抽象類(lèi)負(fù)責(zé)公共構(gòu)造部分的實(shí)現(xiàn),實(shí)現(xiàn)類(lèi)準(zhǔn)確的實(shí)現(xiàn)業(yè)務(wù)邏輯, 同時(shí)在適當(dāng)?shù)臅r(shí)候?qū)Ω割?lèi)進(jìn)行細(xì)化)
說(shuō)了那么多,還是需要深刻的在代碼中多多運(yùn)用“面向接口編程”
OK,接下來(lái)是我們的代碼背景,還是不要上面的手機(jī)例子了, 老衲也是寫(xiě)吐了哈哈- -,這次是英雄聯(lián)盟,恭喜RNG!
主角是咱們的Uzi和香鍋打野
首先聲明英雄和召喚師的接口
interface IHero {
void attack();
}
interface IPlay {
void play();
}
緊接著是咱們的兩個(gè)英雄低層接口
static class Xiazi implements IHero {
@Override
public void attack() {
System.out.println("瞎子,我用雙手,成就你的夢(mèng)想");
}
}
static class VN implements IHero {
@Override
public void attack() {
System.out.println("VN,黑夜也會(huì)怕我");
}
}
然后是咱們的上層player接口
static class ADPlayer implements IPlay {
IHero hero;
public ADPlayer(IHero hero) {
this.hero = hero;
}
@Override
public void play() {
hero.attack();
}
}
static class AssistPlayer implements IPlay {
IHero hero;
public void setHero(IHero hero) {
this.hero = hero;
}
@Override
public void play() {
hero.attack();
}
}
ok,接下來(lái)爽一把
這里注意,咱們的uzi使用構(gòu)造依賴(lài)注入方式,天生的AD,世界第一ADC
咱們的RNG圍繞下路戰(zhàn)術(shù),所以香鍋就一個(gè)使命,保護(hù)下路,使用setter依賴(lài)注入
public static void main(String[] args) {
ADPlayer uzi = new ADPlayer(new VN());
AssistPlayer mlxg = new AssistPlayer();
mlxg.setHero(new Xiazi());
uzi.play();
mlxg.play();
}
輸出
VN,黑夜也會(huì)怕我
瞎子,我用雙手,成就你的夢(mèng)想
再次恭喜RNG集中賽冠軍,不知為何,老衲看小花生就是一臉不爽
接口隔離原則(Interface Splite Principle)
接口 隔離
其實(shí)從字面上就能很好的理解,先不看書(shū),簡(jiǎn)單從字面上理解一下這個(gè)原則
接口:interface(Java 中interface關(guān)鍵字修飾,只能在其中聲明方法/接口和靜態(tài)變量)
類(lèi):class,對(duì)外提供的public方法,從外向內(nèi)看,這其實(shí)也是一種接口
隔離:隔離結(jié)合單一職責(zé)來(lái)看,隔離的基礎(chǔ)首先盡可能保證接口的定義符合單一職責(zé)原則,依據(jù)業(yè)務(wù)劃分出來(lái)的接口功能進(jìn)行進(jìn)一步進(jìn)行拆分細(xì)分,類(lèi)不要去依賴(lài)那些他用不到的接口,不然沒(méi)有意義啊,依賴(lài)那么多搞什么,說(shuō)白了就是對(duì)接口根據(jù)依賴(lài)關(guān)系進(jìn)行一波“抽取”的騷操作
定義
ok,山寨白話(huà)解釋完畢,下面來(lái)對(duì)下文檔,接口描述正確,我們就看一下隔離的解釋
- Clients should not be forced to depend upon interfaces that they don't use.(客戶(hù)端不應(yīng)該依賴(lài)它不需要的接口。)
- The dependency of one class to another one should depend on the smallest possible interface.(類(lèi)間的依賴(lài)關(guān)系應(yīng)該建立在最小的接口上。)
好吧,在下認(rèn)為這個(gè)解釋還不如白話(huà)來(lái)的直接明了,類(lèi)間的依賴(lài)關(guān)系是什么?其實(shí)對(duì)于接口最直接的定義便是類(lèi)之間進(jìn)行通信使用的,那么既然他們之間進(jìn)行通信,那么兩個(gè)類(lèi)之間就存在了耦合關(guān)系,耦合達(dá)到最低要怎么做?就是盡量使這個(gè)耦合接口簡(jiǎn)單明了,那么搬出書(shū)上的解釋?zhuān)?br>
建立單一接口,不要建立臃腫龐大的接口。再通俗一點(diǎn)講:接口盡量細(xì)化,同時(shí)接口中的方法盡量少
code
代碼背景還是咱們LOL,之前是hero,那今天就換成NPC吧
首先來(lái)兩個(gè)接口,分別是魔法攻擊和物理攻擊,也就是咱們的接口隔離
public interface IMagicAttack {
void magicAttack();
}
public interface IPhysicalAttack {
void physicalAttack();
}
接下來(lái)是兩個(gè)咱們的法拉利和遠(yuǎn)程兵,分別實(shí)現(xiàn)魔法和物理攻擊接口
public static class YuanChenBing implements IMagicAttack {
@Override
public void magicAttack() {
System.out.println("遠(yuǎn)程兵,用魔法攻擊轟你家大燈");
}
}
public static class FaLaLi implements IPhysicalAttack {
@Override
public void physicalAttack() {
System.out.println("法拉利炮車(chē),用大炮物理攻擊轟你家大燈");
}
}
最后登場(chǎng)的是大龍,大龍的話(huà)就比較囂張了,必須兩個(gè)攻擊的接口都實(shí)現(xiàn)
public static class DaLong implements IMagicAttack, IPhysicalAttack {
@Override
public void magicAttack() {
System.out.println("大龍向你吐了一口魔法濃痰");
}
@Override
public void physicalAttack() {
System.out.println("大龍用尾巴懟你");
}
}
接下來(lái)run一把瞅瞅
public static void main(String[] args) {
DaLong daLong = new DaLong();
daLong.magicAttack();
daLong.physicalAttack();
YuanChenBing yuanChenBing = new YuanChenBing();
yuanChenBing.magicAttack();
FaLaLi faLaLi = new FaLaLi();
faLaLi.physicalAttack();
}
大龍向你吐了一口魔法濃痰
大龍用尾巴懟你
遠(yuǎn)程兵,用魔法攻擊轟你家大燈
法拉利炮車(chē),用大炮物理攻擊轟你家大燈
代碼擼完了,接下來(lái)搬出書(shū)上的總結(jié),說(shuō)的灰常準(zhǔn)確,請(qǐng)大家注意
- 接口要盡量小,根據(jù)接口隔離原則拆分接口時(shí),首先必須滿(mǎn)足單一職責(zé)原則
- 接口要高內(nèi)聚,什么是高內(nèi)聚?高內(nèi)聚就是提高接口、類(lèi)、模塊的處理能力,減少對(duì)外的交互
- 定制服務(wù),一個(gè)系統(tǒng)或系統(tǒng)內(nèi)的模塊之間必然會(huì)有耦合,有耦合就要有相互訪(fǎng)問(wèn)的接口(并不一定就是Java中定義的Interface,也可能是一個(gè)類(lèi)或單純的數(shù)據(jù)交換),我們?cè)O(shè)計(jì)時(shí)就需要為各個(gè)訪(fǎng)問(wèn)者(即客戶(hù)端)定制服務(wù),什么是定制服務(wù)?定制服務(wù)就是單獨(dú)為一個(gè)個(gè)體提供優(yōu)良的服務(wù)
- 接口設(shè)計(jì)是有限度的,接口的設(shè)計(jì)粒度越小,系統(tǒng)越靈活,這是不爭(zhēng)的事實(shí)。但是,靈活的同時(shí)也帶來(lái)了結(jié)構(gòu)的復(fù)雜化,開(kāi)發(fā)難度增加,可維護(hù)性降低,這不是一個(gè)項(xiàng)目或產(chǎn)品所期望看到的,所以接口設(shè)計(jì)一定要注意適度
- 一個(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ù)邏輯
迪米特法則(Law of Demeter, LoD)
迪米特法則主要表達(dá)的是當(dāng)類(lèi)與類(lèi)之間產(chǎn)生耦合的情況下,類(lèi)對(duì)外公布的方法將遵循怎樣的規(guī)則,其實(shí)說(shuō)白了就是當(dāng)前類(lèi)持有的耦合類(lèi),那么當(dāng)前類(lèi)只關(guān)心自己要調(diào)用的方法,具體內(nèi)部有怎樣的實(shí)現(xiàn)則不關(guān)心,這些不關(guān)心的方法或者變量都與我無(wú)關(guān),這樣寫(xiě)有什么好處呢?相當(dāng)于以后當(dāng)實(shí)現(xiàn)邏輯發(fā)生了變化,但是結(jié)果不變,我們只需要更改耦合類(lèi)的內(nèi)部實(shí)現(xiàn)即可,外部無(wú)需改動(dòng)
定義
一個(gè)對(duì)象應(yīng)該對(duì)其他對(duì)象有最少的了解。通俗地講,
一個(gè)類(lèi)應(yīng)該對(duì)自己需要耦合或調(diào)用的類(lèi)知道得最少,你(被耦合或調(diào)用的類(lèi))的內(nèi)部是如何復(fù)雜都和我沒(méi)關(guān)系,那是你的事情,我就知道你提供的這么多public方法,我就調(diào)用這么多,其他的我一概不關(guān)心。
類(lèi)之間的低耦合要求:
- 類(lèi)與類(lèi)之間的關(guān)系是建立在類(lèi)間的,而不是方法間,因此一個(gè)方法盡量不引入一個(gè)類(lèi)中不存在的對(duì)象
- 迪米特法則要求類(lèi)“羞澀”一點(diǎn),盡量不要對(duì)外公布太多的public方法和非靜態(tài)的public變量,盡量?jī)?nèi)斂,多使用private、package-private、protected等訪(fǎng)問(wèn)權(quán)限
- 如果一個(gè)方法放在本類(lèi)中,既不增加類(lèi)間關(guān)系,也對(duì)本類(lèi)不產(chǎn)生負(fù)面影響,那就放置在本類(lèi)中
- 迪米特法則的核心觀(guān)念就是類(lèi)間解耦,弱耦合,只有弱耦合了以后,類(lèi)的復(fù)用率才可以提高。其要求的結(jié)果就是產(chǎn)生了大量的中轉(zhuǎn)或跳轉(zhuǎn)類(lèi),導(dǎo)致系統(tǒng)的復(fù)雜性提高,同時(shí)也為維護(hù)帶來(lái)了難度。讀者在采用迪米特法則時(shí)需要反復(fù)權(quán)衡,既做到讓結(jié)構(gòu)清晰,又做到高內(nèi)聚低耦合
code
慣例做一個(gè)代碼背景介紹
相信大家都聽(tīng)說(shuō)過(guò)five five open這位兄臺(tái),那么咱們就以這位玩家作為咱們此次Demo的主角來(lái)編寫(xiě)代碼
注意,不對(duì)此人自任何評(píng)判,純粹是講解需要,謝謝
首先咱們來(lái)一波接口,聲明GB該實(shí)現(xiàn)的方法,包括什么自動(dòng)攻擊啊,自動(dòng)躲避技能啊什么的
public interface IShellMethod {
void autoAttack();
void stopAutoAttack();
void autoDucking();
void stopAutoDucking();
void autoUseSkill();
void stopAutoUseSkill();
void autoChangeChangeEquipage();
void stopAutoChangeChangeEquipage();
}
接下來(lái)就是咱們的RMB玩家需要持有的接口就比較簡(jiǎn)單,遵循迪特米法則,就一個(gè)啟動(dòng)和停止
public interface IUseShell {
void enableShell();
void disableShell();
}
然后是咱們的腳本實(shí)現(xiàn)類(lèi),此處同時(shí)實(shí)現(xiàn)RMB玩家的接口,簡(jiǎn)單包裝一下,注意這里增加了一個(gè)內(nèi)部方法為計(jì)算躲避最佳路線(xiàn),符合咱們迪特米法則,外部不關(guān)心內(nèi)部的實(shí)現(xiàn)邏輯
private static class ShellImpl implements IShellMethod, IUseShell {
@Override
public void autoAttack() {
System.out.println("auto attack hero or NPC");
}
@Override
public void stopAutoAttack() {
System.out.println("stop auto attack");
}
@Override
public void autoDucking() {
calculateDuckingPath();
System.out.println("auto dodge attacks");
}
private void calculateDuckingPath() {
System.out.println("calculate best ducking path!");
}
@Override
public void stopAutoDucking() {
System.out.println("stop auto dodge attacks");
}
@Override
public void autoUseSkill() {
System.out.println("auto use hero's skill");
}
@Override
public void stopAutoUseSkill() {
System.out.println("stop auto use hero's skill");
}
@Override
public void autoChangeChangeEquipage() {
System.out.println("auto buy best equipage");
}
@Override
public void stopAutoChangeChangeEquipage() {
System.out.println("stop auto buy best equipage");
}
@Override
public void enableShell() {
this.autoAttack();
this.autoDucking();
this.autoUseSkill();
this.autoChangeChangeEquipage();
}
@Override
public void disableShell() {
this.stopAutoAttack();
this.stopAutoDucking();
this.stopAutoUseSkill();
this.stopAutoChangeChangeEquipage();
}
}
最后則是我們用戶(hù)的包裝類(lèi),持有RMB接口對(duì)象即可
private static class ShellUser {
private IUseShell useShell;
public ShellUser() {
useShell = new ShellImpl();
}
public void startGB() {
System.out.println("開(kāi)始上分");
useShell.enableShell();
}
public void stopGB() {
System.out.println("臥槽,對(duì)面要舉報(bào)我");
useShell.disableShell();
}
}
run一把爽一下
ShellUser lubenwei = new ShellUser();
lubenwei.startGB();
lubenwei.stopGB();
...
開(kāi)始上分
auto attack hero or NPC
calculate best ducking path!
auto dodge attacks
auto use hero's skill
auto buy best equipage
臥槽,對(duì)面要舉報(bào)我
stop auto attack
stop auto dodge attacks
stop auto use hero's skill
stop auto buy best equipage
好了,通過(guò)這個(gè)小Demo大家也可以大致輕松愉快的了解到迪特米法則,迪特米法則不同于上面說(shuō)的幾個(gè)法則,更注重類(lèi)間規(guī)范,是以后耦合類(lèi)間的書(shū)寫(xiě)規(guī)范,很多設(shè)計(jì)模式也是遵循的這些法則組合完成的設(shè)計(jì)。
開(kāi)閉原則(Open-Closed Principle, OCP)
不管你是Java開(kāi)發(fā)還是Android開(kāi)發(fā),只要你曾經(jīng)或者正在學(xué)習(xí)的路上,那么或多或少會(huì)在網(wǎng)上看到這樣的一句話(huà):"對(duì)修改關(guān)閉,對(duì)擴(kuò)展開(kāi)放",OK, what is mean ?
定義
一個(gè)軟件實(shí)體應(yīng)該通過(guò)擴(kuò)展來(lái)實(shí)現(xiàn)變化,而不是通過(guò)修改已有的代碼來(lái)實(shí)現(xiàn)變化。軟件實(shí)體包括以下幾個(gè)部分:
- 項(xiàng)目或軟件產(chǎn)品中按照一定的邏輯規(guī)則劃分的模塊
- 抽象和類(lèi)
- 方法
一個(gè)軟件產(chǎn)品只要在生命期內(nèi),都會(huì)發(fā)生變化,既然變化是一個(gè)既定的事實(shí),我們就應(yīng)該在設(shè)計(jì)時(shí)盡量適應(yīng)這些變化,以提高項(xiàng)目的穩(wěn)定性和靈活性,真正實(shí)現(xiàn)“擁抱變化”。
對(duì)于突如其來(lái)的變化,我們不是以修改原有代碼來(lái)適配新的變化,而是通過(guò)增寫(xiě)擴(kuò)展的方式來(lái)應(yīng)對(duì)這個(gè)新變化。
書(shū)上為這些變化做了一個(gè)歸類(lèi),如下:
邏輯變化
只變化一個(gè)邏輯,而不涉及其他模塊,比如原有的一個(gè)算法是ab+c,現(xiàn)在需要修改為ab*c,可以通過(guò)修改原有類(lèi)中的方法的方式來(lái)完成,前提條件是所有依賴(lài)或關(guān)聯(lián)類(lèi)都按照相同的邏輯處理子模塊變化
一個(gè)模塊變化,會(huì)對(duì)其他的模塊產(chǎn)生影響,特別是一個(gè)低層次的模塊變化必然引起高層模塊的變化,因此在通過(guò)擴(kuò)展完成變化時(shí),高層次的模塊修改是必然的可見(jiàn)視圖變化
注意:
在業(yè)務(wù)規(guī)則改變的情況下高層模塊必須有部分改變以適應(yīng)新業(yè)務(wù),改變要盡量地少,防止變化風(fēng)險(xiǎn)的擴(kuò)散。開(kāi)閉原則對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉,并不意味著不做任何修改,低層模塊的變更,必然要有高層模塊進(jìn)行耦合,否則就是一個(gè)孤立無(wú)意義的代碼片段
項(xiàng)目開(kāi)發(fā)、重構(gòu)、測(cè)試、投產(chǎn)、運(yùn)維,其中的重構(gòu)可以對(duì)原有的設(shè)計(jì)和代碼進(jìn)行修改,運(yùn)維盡量減少對(duì)原有代碼的修改,保持歷史代碼的純潔性,提高系統(tǒng)的穩(wěn)定性。
書(shū)中對(duì)開(kāi)閉原則做了一個(gè)非常好的總結(jié),這里就搬過(guò)來(lái)了:
開(kāi)閉原則是最基礎(chǔ)的一個(gè)原則,前五章節(jié)介紹的原則都是開(kāi)閉原則的具體形態(tài),也就是說(shuō)前五個(gè)原則就是指導(dǎo)設(shè)計(jì)的工具和方法,而開(kāi)閉原則才是其精神領(lǐng)袖。換一個(gè)角度來(lái)理解,依照J(rèn)ava語(yǔ)言的稱(chēng)謂,開(kāi)閉原則是抽象類(lèi),其他五大原則是具體的實(shí)現(xiàn)類(lèi)
那么開(kāi)閉原則又會(huì)帶來(lái)哪些好處呢?
- 首先是“測(cè)試”,在擴(kuò)展的基礎(chǔ)上,測(cè)試只需要測(cè)試新增加的接口就可以,無(wú)需對(duì)之前已經(jīng)穩(wěn)定可靠的代碼進(jìn)行重復(fù)測(cè)試
- 通過(guò)縮小業(yè)務(wù)邏輯粒度從而達(dá)到代碼復(fù)用的作用,從原子邏輯組合成業(yè)務(wù)邏輯,那么原子的拼接組合自然而然能夠產(chǎn)生新的業(yè)務(wù)邏輯,復(fù)用的是久經(jīng)測(cè)試的穩(wěn)定代碼,效率得到很大提升
- 軟件更多的工作其實(shí)是在維護(hù)中,我們寫(xiě)代碼的目的也是為了今后更好的迭代和維護(hù)來(lái)對(duì)代碼進(jìn)行架構(gòu),那么在迭代的過(guò)程中,開(kāi)發(fā)人員可以盡可能的少參與之前代碼的觀(guān)看和理解就能在原有的基礎(chǔ)上進(jìn)行功能的擴(kuò)展,那么這樣的代碼才是良性的代碼,相信同學(xué)們對(duì)閱讀之前的代碼也是或多或少的有過(guò)經(jīng)歷,能深刻體會(huì)其中“奧妙”。
- 代碼的設(shè)計(jì)并不能僅僅局限于當(dāng)前的需求,而是要考慮到將來(lái)的擴(kuò)展和可能的變化,預(yù)留出擴(kuò)展的余地
OK,說(shuō)了這么多關(guān)于開(kāi)閉原則的好處,那么接下來(lái)應(yīng)該提到的是開(kāi)閉原則的使用。
老規(guī)矩,這次的code主角是王者榮耀游戲商城
public interface IGameHero {
int getHeroPrice();
int getDressUpPrice();
String getName();
}
怒上3個(gè)接口規(guī)定商城售賣(mài)英雄的行為,也是定義實(shí)體行為,分別是獲取英雄價(jià)格,獲取英雄皮膚價(jià)格,獲取英雄名字
然后是英雄接口實(shí)現(xiàn)類(lèi)
public static class Hero implements IGameHero {
private int mPrice;
private int mDressPrice;
private String mName;
public Hero(int mPrice, int mDressPrice, String mName) {
this.mPrice = mPrice;
this.mDressPrice = mDressPrice;
this.mName = mName;
}
@Override
public int getHeroPrice() {
return mPrice;
}
@Override
public int getDressUpPrice() {
return mDressPrice;
}
@Override
public String getName() {
return mName;
}
@Override
public String toString() {
return "英雄:" + getName() + "\t英雄價(jià)格:" + getHeroPrice() + "\t皮膚價(jià)格:" + getDressUpPrice();
}
}
接下來(lái)上商店邏輯類(lèi),這里就簡(jiǎn)單寫(xiě)下,通俗易懂
public static class GameStore {
private List<IGameHero> heroes;
public GameStore() {
this.heroes = new ArrayList<>();
heroes.add(new Hero(13888, 888, "白起"));
heroes.add(new Hero(10888, 388, "莊周"));
heroes.add(new Hero(13888, 288, "程咬金"));
heroes.add(new Hero(18888, 688, "貂蟬"));
}
public List<IGameHero> getHeroes() {
return heroes;
}
}
意思通俗易懂,大家自行參閱這些中文式代碼哈~
然后就是我們的main咯~
List<IGameHero> heroes = gameStore.getHeroes();
System.out.println("-----------進(jìn)入商店----------");
final String storeMessage = "售:";
for (IGameHero hero : heroes) {
System.out.println(storeMessage + hero.toString());
}
//輸出
//-----------進(jìn)入商店----------
//售:英雄:白起 英雄價(jià)格:13888 皮膚價(jià)格:888
//售:英雄:莊周 英雄價(jià)格:10888 皮膚價(jià)格:388
//售:英雄:程咬金 英雄價(jià)格:13888 皮膚價(jià)格:288
//售:英雄:貂蟬 英雄價(jià)格:18888 皮膚價(jià)格:688
這就是完美的構(gòu)建了我們的某榮耀的簡(jiǎn)單商城了
好了,接下來(lái)TX要出活動(dòng)了,刺激消費(fèi),掙一波,咋整
很簡(jiǎn)單,針對(duì)咱們的擴(kuò)展開(kāi)放原則,新建一個(gè)英雄實(shí)現(xiàn)接口類(lèi)
public static class OffHero extends Hero {
private float mDiscount = 1.0F;
public OffHero(float discount, int price, int dressPrice, String name) {
super(price, dressPrice, name);
mDiscount = discount;
}
@Override
public int getHeroPrice() {
int ret = (int) (super.getHeroPrice() * mDiscount);
return ret;
}
@Override
public int getDressUpPrice() {
int ret = (int) (super.getDressUpPrice() * mDiscount);
return ret;
}
@Override
public String toString() {
return "折扣英雄:" + super.getName() +
"\t英雄價(jià)格:" + super.getHeroPrice() + ",折扣價(jià)格:" + (int) (super.getHeroPrice() * mDiscount)
+ "\t皮膚價(jià)格:" + super.getDressUpPrice() + ",折扣價(jià)格:" + (int) (super.getDressUpPrice() * mDiscount);
}
}
其實(shí)也就是集成原有的英雄類(lèi),增加一個(gè)折扣屬性,重寫(xiě)獲取英雄價(jià)格和皮膚價(jià)格
然后再略微動(dòng)一下商城類(lèi),增加商城類(lèi)的方法,這一步也是不可避免的,上層增加實(shí)體的獲取和實(shí)現(xiàn)這些代碼是必須要寫(xiě)的。
public void startSale() {
System.out.println("商店開(kāi)始活動(dòng),88折");
this.heroes.clear();
heroes.add(new OffHero(0.88F, 13888, 888, "白起"));
heroes.add(new OffHero(0.88F, 10888, 388, "莊周"));
heroes.add(new OffHero(0.88F, 13888, 288, "程咬金"));
heroes.add(new OffHero(0.88F, 18888, 688, "貂蟬"));
}
public void resetPrice() {
System.out.println("商店折扣活動(dòng)截止");
this.heroes.clear();
heroes.add(new Hero(13888, 888, "白起"));
heroes.add(new Hero(10888, 388, "莊周"));
heroes.add(new Hero(13888, 288, "程咬金"));
heroes.add(new Hero(18888, 688, "貂蟬"));
}
增加兩個(gè)方法,折扣為88折,你買(mǎi)不了吃虧,買(mǎi)不了上當(dāng)~
然后在main里面進(jìn)行調(diào)用
gameStore.startSale();
heroes = gameStore.getHeroes();
for (IGameHero hero : heroes) {
System.out.println(storeMessage + hero.toString());
}
gameStore.resetPrice();
heroes = gameStore.getHeroes();
for (IGameHero hero : heroes) {
System.out.println(storeMessage + hero.toString());
}
//log
//商店開(kāi)始活動(dòng),88折
//售:折扣英雄:白起 英雄價(jià)格:13888,折扣價(jià)格:12221 皮膚價(jià)格:888,折扣價(jià)格:781
//售:折扣英雄:莊周 英雄價(jià)格:10888,折扣價(jià)格:9581 皮膚價(jià)格:388,折扣價(jià)格:341
//售:折扣英雄:程咬金 英雄價(jià)格:13888,折扣價(jià)格:12221 皮膚價(jià)格:288,折扣價(jià)格:253
//售:折扣英雄:貂蟬 英雄價(jià)格:18888,折扣價(jià)格:16621 皮膚價(jià)格:688,折扣價(jià)格:605
//商店折扣活動(dòng)截止
//售:英雄:白起 英雄價(jià)格:13888 皮膚價(jià)格:888
//售:英雄:莊周 英雄價(jià)格:10888 皮膚價(jià)格:388
//售:英雄:程咬金 英雄價(jià)格:13888 皮膚價(jià)格:288
//售:英雄:貂蟬 英雄價(jià)格:18888 皮膚價(jià)格:688
顯而易見(jiàn),很輕松的就達(dá)到了折扣的目的,這就是所謂的抽象原則的擁抱開(kāi)放,關(guān)閉修改