在講設(shè)計(jì)原則之前,我先強(qiáng)制灌輸大家一波雞湯,提倡
面向接口編程,代碼的設(shè)計(jì)更重要的是考慮以后的擴(kuò)展和可維護(hù)性
大家?guī)е@樣的思維來學(xué)習(xí)設(shè)計(jì)模式以及設(shè)計(jì)原則悴晰,慢慢就意會這波毒雞湯了绰筛。
先聲明一點(diǎn)就是老衲的blog枢泰,也是邊學(xué)習(xí),邊記錄铝噩,而后以討論交流的方式敘述衡蚂,有什么不對的地方大家多多擔(dān)待。
創(chuàng)建型-抽象工廠(Abstract Factory)
創(chuàng)建型-工廠方法(Factory Method)
行為型-責(zé)任鏈(Chain of Responsibility)
設(shè)計(jì)原則
單一職責(zé)原則(Single Responsibility Principle骏庸, 簡稱是SRP)
老衲是一位粗鄙之人毛甲,所以描述也是盡可能的白話哈~
定義
白話單一職責(zé):首先,顧名思義具被,什么是單一職責(zé)玻募?就是某玩意,專門負(fù)責(zé)某個東西一姿,就譬如說七咧,你的手機(jī)屏幕,他就只負(fù)責(zé)顯示叮叹,不管顯示app還是視頻還是小黃書艾栋,只要是顯示的活,他就干蛉顽,而且他就只干顯示的活蝗砾,這就是單一職責(zé),那么放到術(shù)語里面就是咱們設(shè)計(jì)的接口或者類携冤,盡量遵循此原則悼粮,有什么好處賴?
- 類的復(fù)雜性降低曾棕, 實(shí)現(xiàn)什么職責(zé)都有清晰明確的定義
- 可讀性提高矮锈, 復(fù)雜性降低, 那當(dāng)然可讀性提高了
- 可維護(hù)性提高睁蕾, 可讀性提高苞笨, 那當(dāng)然更容易維護(hù)了
- 變更引起的風(fēng)險降低, 變更是必不可少的子眶, 如果接口的單一職責(zé)做得好瀑凝, 一個接口修
改只對相應(yīng)的實(shí)現(xiàn)類有影響, 對其他的接口無影響臭杰, 這對系統(tǒng)的擴(kuò)展性粤咪、 維護(hù)性都有非常大
的幫助
這一波好處摘自設(shè)計(jì)模式之禪,總結(jié)下來就是渴杆,看見這個接口所聲明的方法寥枝,你就知道功能都有什么宪塔,初學(xué)者或者初接手的人都可以很快融入到代碼中進(jìn)行迭代和維護(hù)了。
然而道理是這個道理囊拜,但是具體在設(shè)計(jì)代碼的時候某筐,還是要考慮到具體的應(yīng)用下。用書中的話描述就是
單一職責(zé)原則提出了一個編寫程序的標(biāo)準(zhǔn)冠跷, 用“職責(zé)”或“變化原因”來衡量接口或 類設(shè)計(jì)得是否優(yōu)良南誊, 但是“職責(zé)”和“變化原因”都是不可度量的, 因項(xiàng)目而異蜜托, 因環(huán)境而異
code
OK抄囚,結(jié)合上面說的小Demo,接下來來一杯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);
}
上來就是我們的小接口橄务,屏幕顯示幔托,干什么玩意呢?自行翻譯不謝~
然后是我們的實(shí)現(xiàn)類
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)類就是干具體的活了蜂挪,國際慣例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");
}
這一套降龍十八掌走下來結(jié)果:
displayImage:ic_launcher.png
displayText:Hello Done!
displayVideo:xiao huang pian.avi
多囂張重挑?多簡單?當(dāng)后期review一看锅劝,咱們的接口告訴你只負(fù)責(zé)顯示,可以顯示文字蟆湖,圖片和視頻故爵,至于什么時候顯示,顯示什么內(nèi)容隅津,那我不管诬垂,我就顯示,唯一讓我引起變化的是什么伦仍?當(dāng)然是內(nèi)容咯~
總結(jié)一番便是
接口一定要做到單一職責(zé)结窘, 類的設(shè)計(jì)盡量做到只有一個原因引起變化
里氏替換原則(LiskovSubstitution Principle, LSP)
此原則相較于上面的單一職責(zé)充蓝,要復(fù)雜一些隧枫,這里引用書中的原話(一定要認(rèn)真閱讀),后面會通過白話做出相關(guān)解釋哈~
首先要理解的是繼承的特點(diǎn)
- 代碼共享谓苟, 減少創(chuàng)建類的工作量官脓, 每個子類都擁有父類的方法和屬性
- 提高代碼的重用性
- 子類可以形似父類,但又異于父類涝焙, “龍生龍卑笨, 鳳生鳳,老鼠生來會打洞”是說子擁有父的“種”仑撞,“世界上沒有兩片完全相同的葉子”是指明子與父的不同
- 提高代碼的可擴(kuò)展性赤兴,實(shí)現(xiàn)父類的方法就可以“為所欲為”了妖滔,君不見很多開源框架的擴(kuò)展接口都是通過繼承父類來完成的
- 提高產(chǎn)品或項(xiàng)目的開放性
上面是優(yōu)點(diǎn),下面是缺點(diǎn)
- 繼承是侵入性的桶良。 只要繼承座舍, 就必須擁有父類的所有屬性和方法
- 降低代碼的靈活性。子類必須擁有父類的屬性和方法艺普,讓子類自由的世界中多了些約束
- 增強(qiáng)了耦合性簸州。當(dāng)父類的常量、變量和方法被修改時歧譬,需要考慮子類的修改岸浑,而且在缺乏規(guī)范的環(huán)境下,這種修改可能帶來非常糟糕的結(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.(如果對每一個類型為S的對象o1矢洲, 都有類型為T的對象o2, 使得以T定義的所有程序P在所有的對象o1都代換成o2時缩焦, 程序P的行為沒有發(fā)生變
化读虏, 那么類型S是類型T的子類型。 )
- 通俗易懂袁滥,親民定義:
Functions that use pointers or references to base classes must be able to useobjects of derived classes without knowing it.(所有引用基類的地方必須能透明地使用其子類的對象盖桥。 )
只要父類能出現(xiàn)的地方子類就可以出現(xiàn), 而且
替換為子類也不會產(chǎn)生任何錯誤或異常题翻, 使用者可能根本就不需要知道是父類還是子類揩徊。 但
是, 反過來就不行了嵌赠, 有子類出現(xiàn)的地方塑荒, 父類未必就能適應(yīng)。
里氏替換原則為良好的繼承定義了一個規(guī)范姜挺,一句簡單的定義包含了4層含義
- 子類必須完全實(shí)現(xiàn)父類的方法
如果子類不能完整地實(shí)現(xiàn)父類的方法齿税,或者父類的某些方法在子類中已經(jīng)發(fā)生“畸變”,則建議斷開父子繼承關(guān)系炊豪, 采用依賴凌箕、聚集、 組合等關(guān)系代替繼承词渤。
- 子類可以有自己的個性
向下轉(zhuǎn)型(downcast)是不安全的陌知, 從里氏替換原則來看,就是有子類出現(xiàn)的地方父類未必就可以出現(xiàn)
- 覆蓋或?qū)崿F(xiàn)父類的方法時輸入?yún)?shù)可以被放大
里氏替換原則也要求制定一個契約掖肋, 就是父類或接口仆葡,這種設(shè)計(jì)方法也叫做Design by Contract(契約設(shè)計(jì)) ,與里氏替換原則有著異曲同工之妙。 契約制定了沿盅, 也就同時制定了前置條件和后置條件把篓, 前置條件就是你要讓我執(zhí)行,就必須滿足我的條件腰涧; 后置條件就是我執(zhí)行完了需要反饋韧掩, 標(biāo)準(zhǔn)是什么。
- 覆寫或?qū)崿F(xiàn)父類的方法時輸出結(jié)果可以被縮小
采用里氏替換原則的目的就是增強(qiáng)程序的健壯性窖铡,版本升級時也可以保持非常好的兼容性疗锐。即使增加子類,原有的子類還可以繼續(xù)運(yùn)行,每個子類對應(yīng)不同的業(yè)務(wù)含義费彼,使用父類作為參數(shù)滑臊,傳遞不同的子類完成不同的業(yè)務(wù)邏輯
白話方式總結(jié)一下上面的內(nèi)容:
里氏替換原則更像是一種java繼承的規(guī)范用法,"extends"嘛箍铲,大家都用過的東西雇卷,可以這么用那么用隨便用,想用就用颠猴,但是寫完以后發(fā)現(xiàn)沒有卵用关划,用是用了,但是有什么奧妙還是不太清楚翘瓮,為什么要用它贮折?
咱們經(jīng)常談到的一個詞:抽取,譬如說activity资盅,這個活動要setContentView调榄,那個活動也要setContentView,這個活動要上下文律姨,那個活動也要上下文振峻,誒~這時候我們就開始搞一個BaseActivity的東西臼疫,然后讓所有子類去重寫獲取布局ID择份,同時父類直接拿到自己的上下文對象,子類直接使用即可烫堤。
那么是不是我們的活動程序代碼邏輯無論ams(自行百度)怎么使荣赶,你創(chuàng)建的這個活動都好使,符合咱們android體系的健壯性鸽斟,另外你想實(shí)現(xiàn)的黑科技是不是都可以在自己的活動里面去實(shí)現(xiàn)拔创,當(dāng)然這里的設(shè)計(jì)并不是完全符合里氏替換原則,姑且斷章取義富蓄,您就這么斷章取義的理解即可剩燥,另外如果老衲說的有什么不對的地方,歡迎大家指正批評~
還記得上面提到過的Java中繼承帶來的優(yōu)缺點(diǎn)嗎?咱們的里氏替換原則就是一個“揚(yáng)長避短”的做法灭红,具體怎么搞侣滩?code一下見分曉
code
還是手機(jī)的例子,這次咱們不說屏幕变擒,說品牌君珠,先來個抽象手機(jī)
abstract class AbsPhone {
/**
* 使用
*/
public abstract void use();
}
臥槽,就那么簡單娇斑,抽象一個使用的方法~
OK策添,接下來是子類們
/**
* 諾基亞
*/
class NokiaPhone extends AbsPhone {
@Override
public void use() {
System.out.println("砸核桃 實(shí)用戶");
}
}
/**
* 錘子
*/
class TPhone extends AbsPhone {
@Override
public void use() {
System.out.println("錘子 情懷戶");
}
}
/**
* 蘋果
*/
class ApplePhone extends AbsPhone {
private void ringing() {
System.out.println("先讓蘋果特有鈴聲響一陣...嗚嗚嗚");
}
@Override
public void use() {
ringing();
System.out.println("蘋果 zhuang bi 專業(yè)戶");
}
}
很簡單,就是諾基亞毫缆,錘子和蘋果三個街機(jī)唯竹,這里并沒有任何對這些品牌的觀點(diǎn),這是假借名義悔醋,傳輸知識用
再然后必須有一個使用者嘛
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();
}
}
然后我們來用這些兄dei跑一把
public static void main(String[] args) {
Person coke = new Person("庫克");
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
庫克掏出手機(jī)了...
先讓蘋果特有鈴聲響一陣...嗚嗚嗚
蘋果 zhuang bi 專業(yè)戶
老羅掏出手機(jī)了...
錘子 情懷戶
比爾蓋茨掏出手機(jī)了...
砸核桃 實(shí)用戶
爽哉摩窃,爽在哪里了?咱們的person對象只知道自己有個手機(jī)芬骄,這個手機(jī)能執(zhí)行communicate操作猾愿,什么手機(jī)我不管,我只管用它账阻。術(shù)語就是蒂秘,邏輯代碼不管實(shí)現(xiàn),只需要持有著抽象類淘太,然后執(zhí)行抽象類提供的方法即可姻僧,就算以后擴(kuò)展了其他的子類,也不影響我之前的業(yè)務(wù)邏輯蒲牧,子類完全繼承了父類撇贺,同時在不改變方法本身的邏輯下增添了自己的特色,同時也符合父類出現(xiàn)的地方冰抢,就可以替換成子類
請大家仔細(xì)咀嚼下面這兩段話:
采用里氏替換原則的目的就是增強(qiáng)程序的健壯性松嘶, 版本升級時也可以保持非常好的兼容性。 即使增加子類挎扰, 原有的子類還可以繼續(xù)運(yùn)行翠订。 在實(shí)際項(xiàng)目中, 每個子類對應(yīng)不同的業(yè)務(wù)含義遵倦, 使用父類作為參數(shù)尽超, 傳遞不同的子類完成不同的業(yè)務(wù)邏輯
對于基類中定義的所有子程序,用在它的任何一個派生類中時的含義都應(yīng)該是相同的梧躺。這樣繼承才不會增加復(fù)雜度似谁,基類才能真正被復(fù)用,而派生類也能夠在基類的基礎(chǔ)上增加新的行為。如果我們必須要不斷地思考不同派生類的實(shí)現(xiàn)在語義上的差異巩踏,繼承就只會增加復(fù)雜度了斜筐。
本著負(fù)責(zé)人的態(tài)度,老衲還是把樹立的這段話copy過來蛀缝,望大家酌情使用參考:
在項(xiàng)目中顷链,采用里氏替換原則時,盡量避免子類的“個性”屈梁,一旦子類有“個性”嗤练,這個子類和父類之間的關(guān)系就很難調(diào)和了,把子類當(dāng)做父類使用在讶,子類的“個性”被抹殺——委屈了點(diǎn)煞抬;把子類單獨(dú)作為一個業(yè)務(wù)來使用,則會讓代碼間的耦合關(guān)系變得撲朔迷離——缺乏類替換的標(biāo)準(zhǔn)
依賴倒置原則(Dependence Inversion Principle,DIP)
依賴倒置是什么鬼构哺?純說感覺也說不明白革答,白話點(diǎn)來說就是各種注入,依賴的接口注入曙强,就是咱們的接口不依賴實(shí)現(xiàn)残拐,而具體的實(shí)現(xià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)該依賴低層模塊溪食, 兩者都應(yīng)該依賴其抽象
- 抽象不應(yīng)該依賴細(xì)節(jié)
- 細(xì)節(jié)應(yīng)該依賴抽象
定義
抽象就是指接口或抽象類,兩者都是不能直接被實(shí)例化的娜扇;細(xì)節(jié)就是實(shí)現(xiàn)類错沃,實(shí)現(xiàn)接口或繼承抽象類而產(chǎn)生的類就是細(xì)節(jié),其特點(diǎn)就是可以直接被實(shí)例化雀瓢,也就是可以加上一個關(guān)鍵字new產(chǎn)生一個對象
- 模塊間的依賴通過抽象發(fā)生枢析, 實(shí)現(xiàn)類之間不發(fā)生直接的依賴關(guān)系, 其依賴關(guān)系是通過接口或抽象類產(chǎn)生的
- 接口或抽象類不依賴于實(shí)現(xiàn)類
- 實(shí)現(xiàn)類依賴接口或抽象類
那么采用依賴倒置的優(yōu)勢在哪里刃麸?就是
減少類間的耦合性醒叁, 提高系統(tǒng)的穩(wěn)定性,降低并行開發(fā)引起的風(fēng)險嫌蚤,提高代碼的可讀性和可維護(hù)性辐益,穩(wěn)定性較高的設(shè)計(jì)断傲,在周圍環(huán)境頻繁變化的時候脱吱,依然可以做到“我自巋然不動”
那么要如何遵循依賴倒置原則去設(shè)計(jì)代碼呢?首先认罩,我們的始終遵循
- 抽象不依賴細(xì)節(jié)
- 在新增加低層模塊時箱蝠,只修改了業(yè)務(wù)場景類,也就是高層模塊,對其他低層模塊如Driver類不需要做任何修改宦搬,業(yè)務(wù)就可以運(yùn)行牙瓢,把“變更”引起的風(fēng)險擴(kuò)散降到最低
- 如果兩個類直接存在依賴關(guān)系,那么連接他們之間的橋梁就是接口间校,不依賴具體的低層模塊
- 抽象是對實(shí)現(xiàn)的約束矾克,對依賴者而言,也是一種契約憔足,不僅僅約束自己胁附,還同時約束自己與外部的關(guān)系,其目的是保證所有的細(xì)節(jié)不脫離契約的范疇滓彰,確保約束雙方按照既定的契約(抽象)共同發(fā)展控妻,只要抽象這根基線在,細(xì)節(jié)就脫離不了這個圈圈揭绑,始終讓你的對象做到“言必信弓候, 行必果”
- 常用依賴傳遞,只要做到抽象依賴他匪,即使是多層的依賴傳遞也無所畏懼
code
依賴倒置原則的本質(zhì)就是通過抽象(接口或抽象類) 使各個類或模塊的實(shí)現(xiàn)彼此獨(dú)立菇存,
不互相影響, 實(shí)現(xiàn)模塊間的松耦合
- 每個類盡量都有接口或抽象類邦蜜, 或者抽象類和接口兩者都具備
這是依賴倒置的基本要求撰筷, 接口和抽象類都是屬于抽象的, 有了抽象才可能依賴倒置畦徘。 - 變量的表面類型盡量是接口或者是抽象類
- 任何類都不應(yīng)該從具體類派生
- 盡量不要覆寫基類的方法(類間依賴的是抽象毕籽, 覆寫了抽象方法, 對依賴的穩(wěn)定性會產(chǎn)生一定的影響)
- 結(jié)合里氏替換原則使用(接口負(fù)責(zé)定義public屬性和方法井辆, 并且聲明與其他對象的依賴關(guān)系关筒,抽象類負(fù)責(zé)公共構(gòu)造部分的實(shí)現(xiàn),實(shí)現(xiàn)類準(zhǔn)確的實(shí)現(xiàn)業(yè)務(wù)邏輯杯缺, 同時在適當(dāng)?shù)臅r候?qū)Ω割愡M(jìn)行細(xì)化)
說了那么多蒸播,還是需要深刻的在代碼中多多運(yùn)用“面向接口編程”
OK,接下來是我們的代碼背景萍肆,還是不要上面的手機(jī)例子了袍榆, 老衲也是寫吐了哈哈- -,這次是英雄聯(lián)盟塘揣,恭喜RNG包雀!
主角是咱們的Uzi和香鍋打野
首先聲明英雄和召喚師的接口
interface IHero {
void attack();
}
interface IPlay {
void play();
}
緊接著是咱們的兩個英雄低層接口
static class Xiazi implements IHero {
@Override
public void attack() {
System.out.println("瞎子,我用雙手亲铡,成就你的夢想");
}
}
static class VN implements IHero {
@Override
public void attack() {
System.out.println("VN才写,黑夜也會怕我");
}
}
然后是咱們的上層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,接下來爽一把
這里注意葡兑,咱們的uzi使用構(gòu)造依賴注入方式,天生的AD赞草,世界第一ADC
咱們的RNG圍繞下路戰(zhàn)術(shù)讹堤,所以香鍋就一個使命,保護(hù)下路厨疙,使用setter依賴注入
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洲守,黑夜也會怕我
瞎子,我用雙手沾凄,成就你的夢想
再次恭喜RNG集中賽冠軍岖沛,不知為何,老衲看小花生就是一臉不爽
接口隔離原則(Interface Splite Principle)
接口
隔離
其實(shí)從字面上就能很好的理解搭独,先不看書婴削,簡單從字面上理解一下這個原則
接口:interface(Java 中interface關(guān)鍵字修飾,只能在其中聲明方法/接口和靜態(tài)變量)
類:class,對外提供的public方法牙肝,從外向內(nèi)看唉俗,這其實(shí)也是一種接口
隔離:隔離結(jié)合單一職責(zé)來看,隔離的基礎(chǔ)首先盡可能保證接口的定義符合單一職責(zé)原則配椭,依據(jù)業(yè)務(wù)劃分出來的接口功能進(jìn)行進(jìn)一步進(jìn)行拆分細(xì)分虫溜,類不要去依賴那些他用不到的接口,不然沒有意義啊股缸,依賴那么多搞什么衡楞,說白了就是對接口根據(jù)依賴關(guān)系進(jìn)行一波“抽取”的騷操作
定義
ok,山寨白話解釋完畢敦姻,下面來對下文檔瘾境,接口描述正確,我們就看一下隔離的解釋
- Clients should not be forced to depend upon interfaces that they don't use.(客戶端不應(yīng)該依賴它不需要的接口镰惦。)
- The dependency of one class to another one should depend on the smallest possible interface.(類間的依賴關(guān)系應(yīng)該建立在最小的接口上迷守。)
好吧,在下認(rèn)為這個解釋還不如白話來的直接明了旺入,類間的依賴關(guān)系是什么兑凿?其實(shí)對于接口最直接的定義便是類之間進(jìn)行通信使用的,那么既然他們之間進(jìn)行通信茵瘾,那么兩個類之間就存在了耦合關(guān)系礼华,耦合達(dá)到最低要怎么做?就是盡量使這個耦合接口簡單明了拗秘,那么搬出書上的解釋:
建立單一接口圣絮,不要建立臃腫龐大的接口。再通俗一點(diǎn)講:接口盡量細(xì)化聘殖,同時接口中的方法盡量少
code
代碼背景還是咱們LOL晨雳,之前是hero,那今天就換成NPC吧
首先來兩個接口奸腺,分別是魔法攻擊和物理攻擊餐禁,也就是咱們的接口隔離
public interface IMagicAttack {
void magicAttack();
}
public interface IPhysicalAttack {
void physicalAttack();
}
接下來是兩個咱們的法拉利和遠(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("法拉利炮車帮非,用大炮物理攻擊轟你家大燈");
}
}
最后登場的是大龍,大龍的話就比較囂張了讹蘑,必須兩個攻擊的接口都實(shí)現(xiàn)
public static class DaLong implements IMagicAttack, IPhysicalAttack {
@Override
public void magicAttack() {
System.out.println("大龍向你吐了一口魔法濃痰");
}
@Override
public void physicalAttack() {
System.out.println("大龍用尾巴懟你");
}
}
接下來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)程兵末盔,用魔法攻擊轟你家大燈
法拉利炮車,用大炮物理攻擊轟你家大燈
代碼擼完了座慰,接下來搬出書上的總結(jié)陨舱,說的灰常準(zhǔn)確,請大家注意
- 接口要盡量小版仔,根據(jù)接口隔離原則拆分接口時游盲,首先必須滿足單一職責(zé)原則
- 接口要高內(nèi)聚,什么是高內(nèi)聚蛮粮?高內(nèi)聚就是提高接口益缎、類、模塊的處理能力然想,減少對外的交互
- 定制服務(wù)莺奔,一個系統(tǒng)或系統(tǒng)內(nèi)的模塊之間必然會有耦合,有耦合就要有相互訪問的接口(并不一定就是Java中定義的Interface变泄,也可能是一個類或單純的數(shù)據(jù)交換)令哟,我們設(shè)計(jì)時就需要為各個訪問者(即客戶端)定制服務(wù),什么是定制服務(wù)妨蛹?定制服務(wù)就是單獨(dú)為一個個體提供優(yōu)良的服務(wù)
- 接口設(shè)計(jì)是有限度的励饵,接口的設(shè)計(jì)粒度越小,系統(tǒng)越靈活滑燃,這是不爭的事實(shí)役听。但是,靈活的同時也帶來了結(jié)構(gòu)的復(fù)雜化表窘,開發(fā)難度增加典予,可維護(hù)性降低,這不是一個項(xiàng)目或產(chǎn)品所期望看到的乐严,所以接口設(shè)計(jì)一定要注意適度
- 一個接口只服務(wù)于一個子模塊或業(yè)務(wù)邏輯
- 通過業(yè)務(wù)邏輯壓縮接口中的public方法瘤袖,接口時常去回顧,盡量讓接口達(dá)到“滿身筋骨肉”昂验,而不是“肥嘟嘟”的一大堆方法
- 已經(jīng)被污染了的接口捂敌,盡量去修改艾扮,若變更的風(fēng)險較大,則采用適配器模式進(jìn)行轉(zhuǎn)化處理
- 了解環(huán)境占婉,拒絕盲從泡嘴。每個項(xiàng)目或產(chǎn)品都有特定的環(huán)境因素,別看到大師是這樣做的你就照抄逆济。千萬別酌予,環(huán)境不同,接口拆分的標(biāo)準(zhǔn)就不同奖慌。深入了解業(yè)務(wù)邏輯
迪米特法則(Law of Demeter抛虫, LoD)
迪米特法則主要表達(dá)的是當(dāng)類與類之間產(chǎn)生耦合的情況下,類對外公布的方法將遵循怎樣的規(guī)則简僧,其實(shí)說白了就是當(dāng)前類持有的耦合類建椰,那么當(dāng)前類只關(guān)心自己要調(diào)用的方法,具體內(nèi)部有怎樣的實(shí)現(xiàn)則不關(guān)心岛马,這些不關(guān)心的方法或者變量都與我無關(guān)广凸,這樣寫有什么好處呢?相當(dāng)于以后當(dāng)實(shí)現(xiàn)邏輯發(fā)生了變化蛛枚,但是結(jié)果不變谅海,我們只需要更改耦合類的內(nèi)部實(shí)現(xiàn)即可,外部無需改動
定義
一個對象應(yīng)該對其他對象有最少的了解蹦浦。通俗地講扭吁,
一個類應(yīng)該對自己需要耦合或調(diào)用的類知道得最少,你(被耦合或調(diào)用的類)的內(nèi)部是如何復(fù)雜都和我沒關(guān)系盲镶,那是你的事情侥袜,我就知道你提供的這么多public方法,我就調(diào)用這么多溉贿,其他的我一概不關(guān)心枫吧。
類之間的低耦合要求:
- 類與類之間的關(guān)系是建立在類間的,而不是方法間宇色,因此一個方法盡量不引入一個類中不存在的對象
- 迪米特法則要求類“羞澀”一點(diǎn)九杂,盡量不要對外公布太多的public方法和非靜態(tài)的public變量,盡量內(nèi)斂宣蠕,多使用private例隆、package-private、protected等訪問權(quán)限
- 如果一個方法放在本類中抢蚀,既不增加類間關(guān)系镀层,也對本類不產(chǎn)生負(fù)面影響,那就放置在本類中
- 迪米特法則的核心觀念就是類間解耦皿曲,弱耦合唱逢,只有弱耦合了以后吴侦,類的復(fù)用率才可以提高。其要求的結(jié)果就是產(chǎn)生了大量的中轉(zhuǎn)或跳轉(zhuǎn)類坞古,導(dǎo)致系統(tǒng)的復(fù)雜性提高备韧,同時也為維護(hù)帶來了難度。讀者在采用迪米特法則時需要反復(fù)權(quán)衡绸贡,既做到讓結(jié)構(gòu)清晰盯蝴,又做到高內(nèi)聚低耦合
code
慣例做一個代碼背景介紹
相信大家都聽說過five five open這位兄臺毅哗,那么咱們就以這位玩家作為咱們此次Demo的主角來編寫代碼
注意听怕,不對此人自任何評判,純粹是講解需要虑绵,謝謝
首先咱們來一波接口尿瞭,聲明GB該實(shí)現(xiàn)的方法,包括什么自動攻擊啊翅睛,自動躲避技能啊什么的
public interface IShellMethod {
void autoAttack();
void stopAutoAttack();
void autoDucking();
void stopAutoDucking();
void autoUseSkill();
void stopAutoUseSkill();
void autoChangeChangeEquipage();
void stopAutoChangeChangeEquipage();
}
接下來就是咱們的RMB玩家需要持有的接口就比較簡單声搁,遵循迪特米法則,就一個啟動和停止
public interface IUseShell {
void enableShell();
void disableShell();
}
然后是咱們的腳本實(shí)現(xiàn)類捕发,此處同時實(shí)現(xiàn)RMB玩家的接口疏旨,簡單包裝一下,注意這里增加了一個內(nèi)部方法為計(jì)算躲避最佳路線扎酷,符合咱們迪特米法則檐涝,外部不關(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();
}
}
最后則是我們用戶的包裝類,持有RMB接口對象即可
private static class ShellUser {
private IUseShell useShell;
public ShellUser() {
useShell = new ShellImpl();
}
public void startGB() {
System.out.println("開始上分");
useShell.enableShell();
}
public void stopGB() {
System.out.println("臥槽法挨,對面要舉報我");
useShell.disableShell();
}
}
run一把爽一下
ShellUser lubenwei = new ShellUser();
lubenwei.startGB();
lubenwei.stopGB();
...
開始上分
auto attack hero or NPC
calculate best ducking path!
auto dodge attacks
auto use hero's skill
auto buy best equipage
臥槽谁榜,對面要舉報我
stop auto attack
stop auto dodge attacks
stop auto use hero's skill
stop auto buy best equipage
好了,通過這個小Demo大家也可以大致輕松愉快的了解到迪特米法則凡纳,迪特米法則不同于上面說的幾個法則窃植,更注重類間規(guī)范,是以后耦合類間的書寫規(guī)范荐糜,很多設(shè)計(jì)模式也是遵循的這些法則組合完成的設(shè)計(jì)巷怜。
開閉原則(Open-Closed Principle, OCP)
不管你是Java開發(fā)還是Android開發(fā),只要你曾經(jīng)或者正在學(xué)習(xí)的路上暴氏,那么或多或少會在網(wǎng)上看到這樣的一句話:"對修改關(guān)閉丛版,對擴(kuò)展開放",OK偏序, what is mean ?
定義
一個軟件實(shí)體應(yīng)該通過擴(kuò)展來實(shí)現(xiàn)變化页畦,而不是通過修改已有的代碼來實(shí)現(xiàn)變化。軟件實(shí)體包括以下幾個部分:
- 項(xiàng)目或軟件產(chǎn)品中按照一定的邏輯規(guī)則劃分的模塊
- 抽象和類
- 方法
一個軟件產(chǎn)品只要在生命期內(nèi)研儒,都會發(fā)生變化豫缨,既然變化是一個既定的事實(shí)独令,我們就應(yīng)該在設(shè)計(jì)時盡量適應(yīng)這些變化,以提高項(xiàng)目的穩(wěn)定性和靈活性好芭,真正實(shí)現(xiàn)“擁抱變化”燃箭。
對于突如其來的變化,我們不是以修改原有代碼來適配新的變化舍败,而是通過增寫擴(kuò)展的方式來應(yīng)對這個新變化招狸。
書上為這些變化做了一個歸類,如下:
邏輯變化
只變化一個邏輯邻薯,而不涉及其他模塊裙戏,比如原有的一個算法是ab+c,現(xiàn)在需要修改為ab*c厕诡,可以通過修改原有類中的方法的方式來完成累榜,前提條件是所有依賴或關(guān)聯(lián)類都按照相同的邏輯處理子模塊變化
一個模塊變化站刑,會對其他的模塊產(chǎn)生影響凄贩,特別是一個低層次的模塊變化必然引起高層模塊的變化蛀序,因此在通過擴(kuò)展完成變化時溺欧,高層次的模塊修改是必然的可見視圖變化
注意:
在業(yè)務(wù)規(guī)則改變的情況下高層模塊必須有部分改變以適應(yīng)新業(yè)務(wù)讯检,改變要盡量地少芹橡,防止變化風(fēng)險的擴(kuò)散衍腥。開閉原則對擴(kuò)展開放盏缤,對修改關(guān)閉绪穆,并不意味著不做任何修改辨泳,低層模塊的變更,必然要有高層模塊進(jìn)行耦合霞幅,否則就是一個孤立無意義的代碼片段
項(xiàng)目開發(fā)漠吻、重構(gòu)、測試司恳、投產(chǎn)途乃、運(yùn)維,其中的重構(gòu)可以對原有的設(shè)計(jì)和代碼進(jìn)行修改扔傅,運(yùn)維盡量減少對原有代碼的修改耍共,保持歷史代碼的純潔性,提高系統(tǒng)的穩(wěn)定性猎塞。
書中對開閉原則做了一個非常好的總結(jié)试读,這里就搬過來了:
開閉原則是最基礎(chǔ)的一個原則,前五章節(jié)介紹的原則都是開閉原則的具體形態(tài)荠耽,也就是說前五個原則就是指導(dǎo)設(shè)計(jì)的工具和方法钩骇,而開閉原則才是其精神領(lǐng)袖。換一個角度來理解,依照J(rèn)ava語言的稱謂倘屹,開閉原則是抽象類银亲,其他五大原則是具體的實(shí)現(xiàn)類
那么開閉原則又會帶來哪些好處呢?
- 首先是“測試”纽匙,在擴(kuò)展的基礎(chǔ)上务蝠,測試只需要測試新增加的接口就可以,無需對之前已經(jīng)穩(wěn)定可靠的代碼進(jìn)行重復(fù)測試
- 通過縮小業(yè)務(wù)邏輯粒度從而達(dá)到代碼復(fù)用的作用烛缔,從原子邏輯組合成業(yè)務(wù)邏輯馏段,那么原子的拼接組合自然而然能夠產(chǎn)生新的業(yè)務(wù)邏輯,復(fù)用的是久經(jīng)測試的穩(wěn)定代碼践瓷,效率得到很大提升
- 軟件更多的工作其實(shí)是在維護(hù)中院喜,我們寫代碼的目的也是為了今后更好的迭代和維護(hù)來對代碼進(jìn)行架構(gòu),那么在迭代的過程中当窗,開發(fā)人員可以盡可能的少參與之前代碼的觀看和理解就能在原有的基礎(chǔ)上進(jìn)行功能的擴(kuò)展够坐,那么這樣的代碼才是良性的代碼寸宵,相信同學(xué)們對閱讀之前的代碼也是或多或少的有過經(jīng)歷崖面,能深刻體會其中“奧妙”。
- 代碼的設(shè)計(jì)并不能僅僅局限于當(dāng)前的需求梯影,而是要考慮到將來的擴(kuò)展和可能的變化巫员,預(yù)留出擴(kuò)展的余地
OK,說了這么多關(guān)于開閉原則的好處甲棍,那么接下來應(yīng)該提到的是開閉原則的使用简识。
老規(guī)矩,這次的code主角是王者榮耀游戲商城
public interface IGameHero {
int getHeroPrice();
int getDressUpPrice();
String getName();
}
怒上3個接口規(guī)定商城售賣英雄的行為感猛,也是定義實(shí)體行為七扰,分別是獲取英雄價格,獲取英雄皮膚價格陪白,獲取英雄名字
然后是英雄接口實(shí)現(xiàn)類
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英雄價格:" + getHeroPrice() + "\t皮膚價格:" + getDressUpPrice();
}
}
接下來上商店邏輯類颈走,這里就簡單寫下,通俗易懂
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)入商店----------
//售:英雄:白起 英雄價格:13888 皮膚價格:888
//售:英雄:莊周 英雄價格:10888 皮膚價格:388
//售:英雄:程咬金 英雄價格:13888 皮膚價格:288
//售:英雄:貂蟬 英雄價格:18888 皮膚價格:688
這就是完美的構(gòu)建了我們的某榮耀的簡單商城了
好了立由,接下來TX要出活動了,刺激消費(fèi)序厉,掙一波锐膜,咋整
很簡單,針對咱們的擴(kuò)展開放原則弛房,新建一個英雄實(shí)現(xiàn)接口類
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英雄價格:" + super.getHeroPrice() + ",折扣價格:" + (int) (super.getHeroPrice() * mDiscount)
+ "\t皮膚價格:" + super.getDressUpPrice() + ",折扣價格:" + (int) (super.getDressUpPrice() * mDiscount);
}
}
其實(shí)也就是集成原有的英雄類道盏,增加一個折扣屬性,重寫獲取英雄價格和皮膚價格
然后再略微動一下商城類,增加商城類的方法荷逞,這一步也是不可避免的牺堰,上層增加實(shí)體的獲取和實(shí)現(xiàn)這些代碼是必須要寫的。
public void startSale() {
System.out.println("商店開始活動颅围,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("商店折扣活動截止");
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, "貂蟬"));
}
增加兩個方法伟葫,折扣為88折,你買不了吃虧院促,買不了上當(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
//商店開始活動筏养,88折
//售:折扣英雄:白起 英雄價格:13888,折扣價格:12221 皮膚價格:888,折扣價格:781
//售:折扣英雄:莊周 英雄價格:10888,折扣價格:9581 皮膚價格:388,折扣價格:341
//售:折扣英雄:程咬金 英雄價格:13888,折扣價格:12221 皮膚價格:288,折扣價格:253
//售:折扣英雄:貂蟬 英雄價格:18888,折扣價格:16621 皮膚價格:688,折扣價格:605
//商店折扣活動截止
//售:英雄:白起 英雄價格:13888 皮膚價格:888
//售:英雄:莊周 英雄價格:10888 皮膚價格:388
//售:英雄:程咬金 英雄價格:13888 皮膚價格:288
//售:英雄:貂蟬 英雄價格:18888 皮膚價格:688
顯而易見,很輕松的就達(dá)到了折扣的目的常拓,這就是所謂的抽象原則的擁抱開放渐溶,關(guān)閉修改