前言
很久沒有寫博客了,一直給自己找借口說太忙了遍膜,過幾天有空再寫碗硬,幾天之后又幾天,時(shí)間就這么快速的消逝瓢颅。說到底就是自己太懶了恩尾,不下點(diǎn)決心真是不行。我決定逼自己一把挽懦,從今天開始學(xué)習(xí)設(shè)計(jì)模式系列翰意,并寫成博文記錄下來,做不到的話信柿,就罰自己一個(gè)月不玩游戲 (作孽啊冀偶。。渔嚷。进鸠。)
六大原則
言歸正傳,這是我學(xué)習(xí)設(shè)計(jì)模式系列的第一篇文章形病,本文主要講的是面向?qū)ο笤O(shè)計(jì)應(yīng)該遵循的六大原則客年,掌握這些原則能幫助我們更好的理解面向?qū)ο蟮母拍睿材芨玫睦斫庠O(shè)計(jì)模式漠吻。這六大原則分別是:
- 單一職責(zé)原則——SRP
- 開閉原則——OCP
- 里式替換原則——LSP
- 依賴倒置原則——DIP
- 接口隔離原則——ISP
- 迪米特原則——LOD
單一職責(zé)原則
單一職責(zé)原則量瓜,Single Responsibility Principle,簡稱SRP途乃。其定義是應(yīng)該有且僅有一個(gè)類引起類的變更绍傲,這話的意思就是一個(gè)類只擔(dān)負(fù)一個(gè)職責(zé)。
舉個(gè)例子耍共,在創(chuàng)業(yè)公司里烫饼,由于人力成本控制和流程不夠規(guī)范的原因猎塞,往往一個(gè)人需要擔(dān)任N個(gè)職責(zé),一個(gè)工程師可能不僅要出需求枫弟,還要寫代碼邢享,甚至要面談客戶鹏往,光背的鍋就好幾種淡诗,簡單用代碼表達(dá)大概如此:
public class Engineer {
public void makeDemand(){}
public void writeCode(){}
public void meetClient(){}
}
代碼看上去好像沒什么問題,因?yàn)槲覀兤綍r(shí)就是這么寫的啊伊履,但是細(xì)讀一下就能發(fā)現(xiàn)韩容,這種寫法很明顯不符合單一職責(zé)的原則,因?yàn)橐痤惖淖兓恢挥幸粋€(gè)唐瀑,至少有三個(gè)方法都可以引起類的變化群凶,比如有天因?yàn)闃I(yè)務(wù)需要,出需求的方法需要加個(gè)功能 (比如需求的成本分析)哄辣,或者是見客戶也需要個(gè)參數(shù)之類的请梢,那樣一來類的變化就會有多種可能性了,其他引用該類的類也需要相應(yīng)的變化力穗,如果引用類的數(shù)目很多的話毅弧,代碼維護(hù)的成本可想而知會有多高。所以我們需要把這些方法拆分成獨(dú)立的職責(zé)当窗,可以讓一個(gè)類只負(fù)責(zé)一個(gè)方法够坐,每個(gè)類只專心處理自己的方法即可。
單一職責(zé)原則的優(yōu)點(diǎn):
- 類的復(fù)雜性降低崖面,實(shí)現(xiàn)什么職責(zé)都有明確的定義元咙;
- 邏輯變得簡單,類的可讀性提高了巫员,而且庶香,因?yàn)檫壿嫼唵危a的可維護(hù)性也提高了简识;
- 變更的風(fēng)險(xiǎn)降低赶掖,因?yàn)橹粫趩我坏念愔械男薷摹?/li>
開閉原則
開閉原則,Open Closed Principle财异,是Java世界里最基礎(chǔ)的設(shè)計(jì)原則倘零,其定義是:
一個(gè)軟件實(shí)體如類、模塊和函數(shù)應(yīng)該對擴(kuò)展開放戳寸,對修改關(guān)閉
也就是說呈驶,一個(gè)軟件實(shí)體應(yīng)該通過擴(kuò)展來實(shí)現(xiàn)變化,而不是通過修改已有的代碼實(shí)現(xiàn)變化疫鹊。這是為軟件實(shí)體的未來事件而制定的對現(xiàn)行開發(fā)設(shè)計(jì)進(jìn)行約束的一個(gè)原則袖瞻。
在我們編碼的過程中司致,需求變化是不斷的發(fā)生的,當(dāng)我們需要對代碼進(jìn)行修改時(shí)聋迎,我們應(yīng)該盡量做到能不動原來的代碼就不動脂矫,通過擴(kuò)展的方式來滿足需求。
遵循開閉原則的最好手段就是抽象霉晕,例如前面單一職責(zé)原則舉的工程師類庭再,我們說的是把方法抽離成單獨(dú)的類,每個(gè)類負(fù)責(zé)單一的職責(zé)牺堰,但其實(shí)從開閉原則的角度說拄轻,更好的方式是把職責(zé)設(shè)計(jì)成接口,例如把寫代碼的職責(zé)方法抽離成接口的形式伟葫,同時(shí)恨搓,我們在設(shè)計(jì)之初需要考慮到未來所有可能發(fā)生變化的因素,比如未來有可能因?yàn)闃I(yè)務(wù)需要分成后臺和前端的功能筏养,這時(shí)設(shè)計(jì)之初就可以設(shè)計(jì)成兩個(gè)接口斧抱,
public interface BackCode{
void writeCode();
}
public interface FrontCode{
void writeCode();
}
如果將來前端代碼的業(yè)務(wù)發(fā)生變化,我們只需擴(kuò)展前端接口的功能渐溶,或者修改前端接口的實(shí)現(xiàn)類即可辉浦,后臺接口以及實(shí)現(xiàn)類就不會受到影響,這就是抽象的好處掌猛。
里氏替換原則
里氏替換原則盏浙,英文名Liskov Substitution Principle,它的定義是
如果對每一個(gè)類型為T1的對象o1荔茬,都有類型為T2的對象o2废膘,使得以T1定義的所有程序P在所有對象o1都替換成o2的時(shí)候,程序P的行為都沒有發(fā)生變化慕蔚,那么類型T2是類型T1的子類型丐黄。
看起來有點(diǎn)繞口,它還有一個(gè)簡單的定義:
所有引用基類的地方必須能夠透明地使用其子類的對象孔飒。
通俗點(diǎn)說灌闺,只要父類能出現(xiàn)的地方子類就可以出現(xiàn),而且替換為子類也不會產(chǎn)生任何異常坏瞄。 但是反過來就不行了桂对,因?yàn)樽宇惪梢詳U(kuò)展父類沒有的功能,同時(shí)子類還不能改變父類原有的功能鸠匀。
我們都知道蕉斜,面向?qū)ο蟮娜筇卣魇欠庋b、繼承和多態(tài),這三者缺一不可宅此,但三者之間卻并不 “和諧“机错。因?yàn)槔^承有很多缺點(diǎn),當(dāng)子類繼承父類時(shí)父腕,雖然可以復(fù)用父類的代碼弱匪,但是父類的屬性和方法對子類都是透明的,子類可以隨意修改父類的成員璧亮。如果需求變更萧诫,子類對父類的方法進(jìn)行了一些復(fù)寫的時(shí)候,其他的子類可能就需要隨之改變杜顺,這在一定程度上就違反了封裝的原則财搁,解決的方案就是引入里氏替換原則蘸炸。
里氏替換原則為良好的繼承定義了一個(gè)規(guī)范躬络,它包含了4層含義:
1、子類可以實(shí)現(xiàn)父類的抽象方法搭儒,但是不能覆蓋父類的非抽象方法穷当。
2、子類可以有自己的個(gè)性淹禾,可以有自己的屬性和方法馁菜。
3、子類覆蓋或重載父類的方法時(shí)輸入?yún)?shù)可以被放大铃岔。
比如父類有一個(gè)方法汪疮,參數(shù)是HashMap
public class Father {
public void test(HashMap map){
System.out.println("父類被執(zhí)行。毁习。智嚷。。纺且。");
}
}
那么子類的同名方法輸入?yún)?shù)的類型可以擴(kuò)大盏道,例如我們輸入?yún)?shù)為Map,
public class Son extends Father{
public void test(Map map){
System.out.println("子類被執(zhí)行载碌。猜嘱。。嫁艇。");
}
}
我們寫一個(gè)場景類測試一下父類的方法執(zhí)行效果朗伶,
public class Client {
public static void main(String[] args) {
Father father = new Father();
HashMap map = new HashMap();
father.test(map);
}
}
結(jié)果輸出:父類被執(zhí)行。步咪。论皆。。。
因?yàn)槔锸咸鎿Q原則纯丸,只要父類能出現(xiàn)的地方子類就可以出現(xiàn)偏形,而且替換為子類也不會產(chǎn)生任何異常。我們改下代碼觉鼻,調(diào)用子類的方法俊扭,
public class Client {
public static void main(String[] args) {
Son son = new Son();
HashMap map = new HashMap();
father.test(map);
}
}
運(yùn)行結(jié)果是一樣的,因?yàn)樽宇惙椒ǖ妮斎雲(yún)?shù)類型范圍擴(kuò)大了坠陈,子類代替父類傳遞到調(diào)用者中萨惑,子類的方法永遠(yuǎn)不會被執(zhí)行,這樣的結(jié)果其實(shí)是正確的仇矾,如果想讓子類方法執(zhí)行庸蔼,可以重寫方法體。
反之贮匕,如果子類的輸入?yún)?shù)類型范圍比父類還小姐仅,比如父類中的參數(shù)是Map,而子類是HashMap刻盐,那么執(zhí)行上述代碼的結(jié)果就會是子類的方法體掏膏,有人說,這難道不對嗎敦锌?子類顯示自己的內(nèi)容啊馒疹。其實(shí)這是不對的,因?yàn)樽宇悰]有復(fù)寫父類的同名方法乙墙,方法就被執(zhí)行了颖变,這會引起邏輯的混亂,如果父類是抽象類,子類是實(shí)現(xiàn)類,你傳遞一個(gè)這樣的實(shí)現(xiàn)類就違背了父類的意圖了展箱,容易引起邏輯混亂朱庆,所以子類覆蓋或重載父類的方法時(shí)輸入?yún)?shù)必定是相同或者放大的。
4、子類覆蓋或重載父類的方法時(shí)輸出結(jié)果可以被縮小,也就是說返回值要小于或等于父類的方法返回值。
確保程序遵循里氏替換原則可以要求我們的程序建立抽象朽色,通過抽象去建立規(guī)范,然后用實(shí)現(xiàn)去擴(kuò)展細(xì)節(jié)组题,所以葫男,它跟開閉原則往往是相互依存的。
依賴倒置原則
依賴倒置原則崔列,Dependence Inversion Principle梢褐,簡稱DIP旺遮,它的定義是:
高層模塊不應(yīng)該依賴底層模塊,兩者都應(yīng)該依賴其抽象盈咳;
抽象不應(yīng)該依賴細(xì)節(jié)耿眉;
細(xì)節(jié)應(yīng)該依賴抽象;
什么是高層模塊和底層模塊呢鱼响?不可分割的原子邏輯就是底層模塊鸣剪,原子邏輯的再組裝就是高層模塊。
在Java語言中丈积,抽象就是指接口或抽象類筐骇,兩者都不能被實(shí)例化;而細(xì)節(jié)就是實(shí)現(xiàn)接口或繼承抽象類產(chǎn)生的類江滨,也就是可以被實(shí)例化的實(shí)現(xiàn)類铛纬。依賴倒置原則是指模塊間的依賴是通過抽象來發(fā)生的,實(shí)現(xiàn)類之間不發(fā)生直接的依賴關(guān)系唬滑,其依賴關(guān)系是通過接口是來實(shí)現(xiàn)的告唆,這就是俗稱的面向接口編程。
我們用歌手唱歌來舉例间雀,比如一個(gè)歌手唱國語歌悔详,用代碼表示就是:
public class ChineseSong {
public String language() {
return "國語歌";
}
}
public class Singer {
//唱歌的方法
public void sing(ChineseSong song) {
System.out.println("歌手" + song.language());
}
}
public class Client {
public static void main(String[] args) {
Singer singer = new Singer();
ChineseSong song = new ChineseSong();
singer.sing(song);
}
}
運(yùn)行main方法,結(jié)果就會輸出:歌手唱國語歌
現(xiàn)在惹挟,我們需要給歌手加一點(diǎn)難度,比如說唱英文歌缝驳,在這個(gè)類中连锯,我們發(fā)現(xiàn)是很難做的。因?yàn)槲覀僑inger類依賴于一個(gè)具體的實(shí)現(xiàn)類ChineseSong用狱,也許有人會說可以在加一個(gè)方法啊运怖,但這樣一來我們就修改了Singer類了,如果以后需要增加更多的歌種夏伊,那歌手類不是一直要被修改摇展?也就是說,依賴類已經(jīng)不穩(wěn)定了溺忧,這顯然不是我們想看到的咏连。
所以我們需要用面向接口編程的思想來優(yōu)化我們的方案,改成如下的代碼:
public interface Song {
public String language();
}
public class ChineseSong implements Song{
public String language() {
return "唱國語歌";
}
}
public class EnglishSong implements Song {
public String language() {
return "唱英語歌";
}
}
public class Singer {
//唱歌的方法
public void sing(Song song) {
System.out.println("歌手" + song.language());
}
}
public class Client {
public static void main(String[] args) {
Singer singer = new Singer();
EnglishSong englishSong = new EnglishSong();
// 唱英文歌
singer.sing(englishSong);
}
}
我們把歌單獨(dú)抽成一個(gè)接口Song
鲁森,每個(gè)歌種都實(shí)現(xiàn)該接口并重寫方法祟滴,這樣一來,歌手的代碼不必改動歌溉,如果需要添加歌的種類垄懂,只需寫多一個(gè)實(shí)現(xiàn)類繼承Song
即可。
通過這樣的面向接口編程,我們的代碼就有了更好的擴(kuò)展性草慧,同時(shí)也降低了耦合桶蛔,提高了系統(tǒng)的穩(wěn)定性。
接口隔離原則
接口隔離原則漫谷,Interface Segregation Principle羽圃,簡稱ISP,其定義是:
客戶端不應(yīng)該依賴它不需要的接口
意思就是客戶端需要什么接口就提供什么接口抖剿,把不需要的接口剔除掉朽寞,這就需要對接口進(jìn)行細(xì)化,保證接口的純潔性斩郎。換成另一種說法就是脑融,類間的依賴關(guān)系應(yīng)該建立在最小的接口上,也就是建立單一的接口缩宜。
你可能會疑惑肘迎,建立單一接口,這不是單一職責(zé)原則嗎锻煌?其實(shí)不是妓布,單一職責(zé)原則要求的是類和接口職責(zé)單一,注重的是職責(zé)宋梧,一個(gè)職責(zé)的接口是可以有多個(gè)方法的匣沼,而接口隔離原則要求的是接口的方法盡量少,模塊盡量單一捂龄,如果需要提供給客戶端很多的模塊释涛,那么就要相應(yīng)的定義多個(gè)接口,不要把所有的模塊功能都定義在一個(gè)接口中倦沧,那樣會顯得很臃腫唇撬。
舉個(gè)例子,現(xiàn)在的智能手機(jī)非常的發(fā)達(dá)展融,幾乎是人手一部的社會狀態(tài)窖认,在我們年輕人的觀念里,好的智能手機(jī)應(yīng)該是價(jià)格便宜告希,外觀好看扑浸,功能豐富的,由此我們可以定義一個(gè)智能手機(jī)的抽象接口 ISmartPhone暂雹,代碼如下所示:
public interface ISmartPhone {
public void cheapPrice();
public void goodLooking();
public void richFunction();
}
接著首装,我們定義一個(gè)手機(jī)接口的實(shí)現(xiàn)類,實(shí)現(xiàn)這三個(gè)抽象方法杭跪,
public class SmartPhone implements ISmartPhone{
public void cheapPrice() {
System.out.println("這手機(jī)便宜~~~~~");
}
public void goodLooking() {
System.out.println("這手機(jī)外觀好看~~~~~");
}
public void richFunction() {
System.out.println("這手機(jī)功能真多~~~~~");
}
}
然后仙逻,定義一個(gè)用戶的實(shí)體類 User驰吓,并定義一個(gè)構(gòu)造方法,以ISmartPhone 作為參數(shù)傳入系奉,同時(shí)檬贰,我們也定義一個(gè)使用的方法usePhone 來調(diào)用接口的方法,
public class User {
private ISmartPhone phone;
public User(ISmartPhone phone){
this.phone = phone;
}
public void usePhone(){
phone.cheapPrice();
phone.goodLooking();
phone.richFunction();
}
}
可以看出缺亮,當(dāng)我們實(shí)例化User
類并調(diào)用其方法usePhone
后翁涤,控制臺上就會顯示手機(jī)接口三個(gè)方法的方法體信息,這種設(shè)計(jì)看上去沒什么大毛病萌踱,但是我們可以仔細(xì)想下葵礼,ISmartPhone這個(gè)接口的設(shè)計(jì)是否已經(jīng)達(dá)到最優(yōu)了呢?很遺憾并鸵,答案是沒有鸳粉,接口其實(shí)還可以再優(yōu)化。
因?yàn)槌四贻p人之外园担,中年商務(wù)人士也在用智能手機(jī)届谈,在他們的觀念里,智能手機(jī)并不需要豐富的功能弯汰,甚至不用考慮是否便宜 (有錢就是任性~~~~)艰山,因?yàn)槌晒θ耸慷急容^忙,對智能手機(jī)的要求大多是外觀大氣咏闪,功能簡單即可曙搬,這才是他們心中好的智能手機(jī)的特征,這樣一來汤踏,我們定義的 ISmartPhone 接口就無法適用了织鲸,因?yàn)槲覀兊慕涌诙x了智能手機(jī)必須滿足三個(gè)特性,如果實(shí)現(xiàn)該接口就必須三個(gè)方法都實(shí)現(xiàn)溪胶,而對商務(wù)人員的標(biāo)準(zhǔn)來說,我們定義的方法只有外觀符合且可以重用而已稳诚。你可能會說哗脖,我可以重寫一個(gè)實(shí)現(xiàn)類啊,只實(shí)現(xiàn)外觀的方法扳还,另外兩個(gè)方法置空才避,什么都不寫,這不就行了嗎氨距?但是這也不行桑逝,因?yàn)?User 引用的是ISmartPhone 接口,它調(diào)用三個(gè)方法俏让,你只實(shí)現(xiàn)了兩個(gè)楞遏,那么打印信息就少了兩條了茬暇,只靠外觀的特性,使用者怎么知道智能手機(jī)是否符合自己的預(yù)期寡喝?
分析到這里糙俗,我們大概就明白了,其實(shí)ISmartPhone的設(shè)計(jì)是有缺陷的预鬓,過于臃腫了巧骚,按照接口隔離原則,我們可以根據(jù)不同的特性把智能手機(jī)的接口進(jìn)行拆分格二,這樣一來劈彪,每個(gè)接口的功能就會變得單一,保證了接口的純潔性顶猜,也進(jìn)一步提高了代碼的靈活性和穩(wěn)定性沧奴。
迪米特原則
迪米特原則,Law of Demeter驶兜,簡稱LoD扼仲,也被稱為最少知識原則,它描述的規(guī)則是:
一個(gè)對象應(yīng)該對其他對象有最少的了解
也就是說抄淑,一個(gè)類應(yīng)該對自己需要耦合或調(diào)用的類知道的最少屠凶,類與類之間的關(guān)系越密切,耦合度越大肆资,那么類的變化對其耦合的類的影響也會越大矗愧,這也是我們面向設(shè)計(jì)的核心原則:低耦合,高內(nèi)聚郑原。
迪米特法則還有一個(gè)解釋:只與直接的朋友通信唉韭。
什么是直接的朋友呢?每個(gè)對象都必然與其他對象有耦合關(guān)系犯犁,兩個(gè)對象的耦合就成為朋友關(guān)系属愤,這種關(guān)系的類型很多,例如組合酸役、聚合住诸、依賴等。其中涣澡,我們稱出現(xiàn)成員變量贱呐、方法參數(shù)、方法返回值中的類為直接的朋友入桂,而出現(xiàn)在局部變量中的類則不是直接的朋友奄薇。也就是說,陌生的類最好不要作為局部變量的形式出現(xiàn)在類的內(nèi)部抗愁。
舉個(gè)例子馁蒂,上體育課之前呵晚,老師讓班長先去體務(wù)室拿20個(gè)籃球,等下上課的時(shí)候要用远搪。根據(jù)這一場景劣纲,我們可以設(shè)計(jì)出三個(gè)類 Teacher(老師),Monitor (班長) 和 BasketBall (籃球)谁鳍,以及發(fā)布命令的方法command
和 拿籃球的方法takeBall
癞季,
public class Teacher {
// 命令班長去拿球
public void command(Monitor monitor) {
List<BasketBall> ballList = new ArrayList<BasketBall>();
// 初始化籃球數(shù)目
for (int i = 0;i<20;i++){
ballList.add(new BasketBall());
}
// 通知班長開始去拿球
monitor.takeBall(ballList);
}
}
public class BasketBall {
}
public class Monitor {
// 拿球
public void takeBall(List<BasketBall> balls) {
System.out.println("籃球數(shù)目:" + balls.size());
}
}
然后,我們寫一個(gè)情景類進(jìn)行測試:
public class Client {
public static void main(String[] args) {
Teacher teacher = new Teacher();
teacher.command(new Monitor());
}
}
結(jié)果顯示如下:
籃球數(shù)目:20
雖然結(jié)果是正確的倘潜,但我們的程序其實(shí)還是存在問題绷柒,因?yàn)閺膱鼍皝碚f,老師只需命令班長拿籃球即可涮因,Teacher只需要一個(gè)朋友----Monitor废睦,但在程序里,Teacher的方法體中卻依賴了BasketBall類养泡,也就是說嗜湃,Teacher類與一個(gè)陌生的類有了交流,這樣Teacher的健壯性就被破壞了澜掩,因?yàn)橐坏〣asketBall類做了修改购披,那么Teacher也需要做修改,這很明顯違背了迪米特法則肩榕。
因此刚陡,我們需要對程序做些修改,在Teacher的方法中去掉對BasketBall類的依賴株汉,只讓Teacher類與朋友類Monitor產(chǎn)生依賴筐乳,修改后的代碼如下:
public class Teacher {
// 命令班長去拿球
public void command(Monitor monitor) {
// 通知班長開始去拿球
monitor.takeBall();
}
}
public class Monitor {
// 拿球
public void takeBall() {
List<BasketBall> ballList = new ArrayList<BasketBall>();
// 初始化籃球數(shù)目
for (int i = 0;i<20;i++){
ballList.add(new BasketBall());
}
System.out.println("籃球數(shù)目:" + ballList.size());
}
}
這樣一來,Teacher類就不會與BasketBall類產(chǎn)生依賴了乔妈,即時(shí)日后因?yàn)闃I(yè)務(wù)需要修改BasketBall也不會影響Teacher類蝙云。
總結(jié)
好了,面向?qū)ο蟮牧笤瓌t就介紹到這里了路召。其實(shí)贮懈,我們不難發(fā)現(xiàn),六大原則雖說是原則优训,但它們并不是強(qiáng)制性的,更多的是建議各聘。遵照這些原則固然能幫助我們更好的規(guī)范我們的系統(tǒng)設(shè)計(jì)和代碼習(xí)慣揣非,但并不是所有的場景都適用,就例如接口隔離原則躲因,在現(xiàn)實(shí)系統(tǒng)開發(fā)中早敬,我們很難完全遵守一個(gè)模塊一個(gè)接口的設(shè)計(jì)忌傻,否則業(yè)務(wù)多了就會出現(xiàn)代碼設(shè)計(jì)過度的情況,讓整個(gè)系統(tǒng)變得過于龐大搞监,增加了系統(tǒng)的復(fù)雜度水孩,甚至影響自己的項(xiàng)目進(jìn)度,得不償失啊琐驴。
所以俘种,還是那句話,在合適的場景選擇合適的技術(shù)绝淡!
參考:《設(shè)計(jì)模式之禪》