寫(xiě)了這么多年代碼项秉,你真的了解SOLID嗎绣溜?

盡管大家都認(rèn)為SOLID是非常重要的設(shè)計(jì)原則,并且對(duì)每一條原則都耳熟能詳娄蔼,但我發(fā)現(xiàn)大部分開(kāi)發(fā)者并沒(méi)有真正理解怖喻。要獲得最大收益,就必須理解它們之間的關(guān)系岁诉,并綜合應(yīng)用所有這些原則锚沸。只有把SOLID作為一個(gè)整體,才可能構(gòu)建出堅(jiān)實(shí)(Solid)的軟件涕癣。遺憾的是哗蜈,我們看到的書(shū)籍和文章都在羅列每個(gè)原則,沒(méi)有把它們作為一個(gè)整體來(lái)看坠韩,甚至提出SOLID原則的Bob大叔也沒(méi)能講透徹距潘。因此我嘗試介紹一下我的理解。

先拋出我的觀點(diǎn): 單一職責(zé)是所有設(shè)計(jì)原則的基礎(chǔ)只搁,開(kāi)閉原則是設(shè)計(jì)的終極目標(biāo)音比。里氏替換原則強(qiáng)調(diào)的是子類(lèi)替換父類(lèi)后程序運(yùn)行時(shí)的正確性,它用來(lái)幫助實(shí)現(xiàn)開(kāi)閉原則氢惋。而接口隔離原則用來(lái)幫助實(shí)現(xiàn)里氏替換原則洞翩,同時(shí)它也體現(xiàn)了單一職責(zé)。依賴倒置原則是過(guò)程式編程與OO編程的分水嶺焰望,同時(shí)它也被用來(lái)指導(dǎo)接口隔離原則骚亿。關(guān)系如下圖:


單一職責(zé)原則(Single Responsibility Principle)

單一職責(zé)是最容易理解的設(shè)計(jì)原則,但也是被違反得最多的設(shè)計(jì)原則之一熊赖。

要真正理解并正確運(yùn)用單一職責(zé)原則循未,并沒(méi)有那么容易。單一職責(zé)就跟“鹽少許”一樣,不好把握的妖。Robert C. Martin(又名“Bob大叔”)把職責(zé)定義為變化原因绣檬,將單一職責(zé)描述為 ”A class should have only one reason to change." 也就是說(shuō),如果有多種變化原因?qū)е乱粋€(gè)類(lèi)要修改嫂粟,那么這個(gè)類(lèi)就違反了單一職責(zé)原則娇未。那么問(wèn)題來(lái)了,什么是“變化原因”呢星虹?

利益相關(guān)者角色是一個(gè)重要的變化原因零抬,不同的角色會(huì)有不同的需求,從而產(chǎn)生不同的變化原因宽涌。作為居民平夜,家用的電線是普通的220V電線,而對(duì)電網(wǎng)建設(shè)者卸亮,使用的是高壓電線忽妒。用一個(gè)Wire類(lèi)同時(shí)服務(wù)于兩類(lèi)角色,通常意味著壞味道兼贸。

變更頻率是另一個(gè)值得考慮的變化原因段直。即使對(duì)同一類(lèi)角色,需求變更的頻率也會(huì)存在差異溶诞。最典型的例子是業(yè)務(wù)處理的需求比較穩(wěn)定鸯檬,而業(yè)務(wù)展示的需求更容易發(fā)生變更,畢竟人總是喜新厭舊的螺垢。因此這兩類(lèi)需求通常要在不同的類(lèi)中實(shí)現(xiàn)喧务。

單一職責(zé)原則某種程度上說(shuō)是在分離關(guān)注點(diǎn)。分離不同角色的關(guān)注點(diǎn)枉圃,分離不同時(shí)間的關(guān)注點(diǎn)功茴。

在實(shí)踐中,怎么運(yùn)用單一職責(zé)原則呢讯蒲?什么時(shí)候要拆分痊土,什么時(shí)候要合并?我們看看新廚師在學(xué)炒菜時(shí)墨林,是如何掌握“鹽少許”的赁酝。他會(huì)不斷地品嘗,直到味道剛好為止旭等。寫(xiě)代碼也一樣酌呆,你需要識(shí)別需求變化的信號(hào),不斷“品嘗”你的代碼搔耕,當(dāng)“味道”不夠好時(shí)隙袁,持續(xù)重構(gòu)痰娱,直到“味道”剛剛好。

開(kāi)閉原則(Open-closed Principle)

開(kāi)閉原則指軟件實(shí)體(類(lèi)菩收、模塊等)應(yīng)當(dāng)對(duì)擴(kuò)展開(kāi)放梨睁,對(duì)修改閉合。這聽(tīng)起來(lái)似乎很不合理娜饵,不能修改坡贺,只能擴(kuò)展?那我怎么寫(xiě)代碼箱舞?

我們先看看為什么要有開(kāi)閉原則遍坟。假設(shè)你是一名成功的開(kāi)源類(lèi)庫(kù)作者,很多開(kāi)發(fā)者使用你的類(lèi)庫(kù)晴股。如果某天你要擴(kuò)展功能愿伴,只能通過(guò)修改某些代碼完成,結(jié)果導(dǎo)致類(lèi)庫(kù)的使用者都需要修改代碼电湘。更可怕的是隔节,他們被迫修改了代碼后,又可能造成別的依賴者也被迫修改代碼胡桨。這種場(chǎng)景絕對(duì)是一場(chǎng)災(zāi)難官帘。

如果你的設(shè)計(jì)是滿足開(kāi)閉原則的瞬雹,那就完全是另一種場(chǎng)景昧谊。你可以通過(guò)擴(kuò)展,而不是修改來(lái)改變軟件的行為酗捌,將對(duì)依賴方的影響降到最低呢诬。

這不正是設(shè)計(jì)的終極目標(biāo)嗎?解耦胖缤、高內(nèi)聚尚镰、低耦合等等設(shè)計(jì)原則最終不都是為了這個(gè)目標(biāo)嗎?暢想一下哪廓,類(lèi)狗唉、模塊、服務(wù)都不需要修改涡真,而是通過(guò)擴(kuò)展就能夠改變其行為分俯。就像計(jì)算機(jī)一樣,組件可以輕松擴(kuò)展哆料。硬盤(pán)太懈准簟?直接換個(gè)大的东亦,顯示器不夠大的杏节?來(lái)個(gè)8K的怎么樣?

什么時(shí)候應(yīng)該應(yīng)用開(kāi)閉原則,怎么做到呢奋渔?沒(méi)有人能夠在一開(kāi)始就識(shí)別出所有擴(kuò)展點(diǎn)镊逝,也不可能在所有地方都預(yù)留出擴(kuò)展點(diǎn),這么做的成本是不可接受的嫉鲸。因此一定是由需求變化驅(qū)動(dòng)蹋半。如果你有領(lǐng)域?qū)<业闹С郑梢詭湍阕R(shí)別出變化點(diǎn)充坑。否則减江,你應(yīng)該在變化發(fā)生時(shí)來(lái)做決策,因?yàn)樵跊](méi)有任何依據(jù)時(shí)做過(guò)多預(yù)先設(shè)計(jì)違反了Yagni捻爷。

實(shí)現(xiàn)開(kāi)閉原則的關(guān)鍵是抽象辈灼。在Bertrand Meyer提出開(kāi)閉原則的年代(上世紀(jì)80年代),在類(lèi)庫(kù)中增加屬性或方法也榄,都不可避免地要修改依賴此類(lèi)庫(kù)的代碼巡莹。這顯然導(dǎo)致軟件很難維護(hù),因此他強(qiáng)調(diào)的是要允許通過(guò)繼承來(lái)擴(kuò)展類(lèi)甜紫。隨著技術(shù)發(fā)展降宅,我們有了更多的方法來(lái)實(shí)現(xiàn)開(kāi)閉原則,包括接口囚霸、抽象類(lèi)腰根、策略模式等。

我們也許永遠(yuǎn)都無(wú)法完全做到開(kāi)閉原則拓型,但不妨礙它是設(shè)計(jì)的終極目標(biāo)额嘿。SOLID的其它原則都直接或間接為開(kāi)閉原則服務(wù),例如接下來(lái)要介紹的里氏替換原則劣挫。

里氏替換原則 (The Liskov Substitution Principle)

里氏替換原則說(shuō)的是派生類(lèi)(子類(lèi))對(duì)象能夠替換其基類(lèi)(父類(lèi))對(duì)象被使用册养。學(xué)過(guò)OO的同學(xué)都知道,子類(lèi)本來(lái)就可以替換父類(lèi)压固,為什么還要里氏替換原則呢球拦?這里強(qiáng)調(diào)的不是編譯錯(cuò)誤,而是程序運(yùn)行時(shí)的正確性帐我。

程序運(yùn)行的正確性通晨擦叮可以分為兩類(lèi)。一類(lèi)是不能出現(xiàn)運(yùn)行時(shí)異常焚刚,最典型的是UnsupportedOperationException点弯,也就是子類(lèi)不支持父類(lèi)的方法。第二類(lèi)是業(yè)務(wù)的正確性矿咕,這取決于業(yè)務(wù)上下文抢肛。

下例中狼钮,由于java.sql.Date不支持父類(lèi)的toInstance方法,當(dāng)父類(lèi)被它替換時(shí)捡絮,程序無(wú)法正常運(yùn)行熬芜,破壞了父類(lèi)與調(diào)用方的契約,因此違反了里氏替換原則福稳。

package java.sql;

public class Date extends java.util.Date {
    @Override
    public Instant toInstant() {
        throw new java.lang.UnsupportedOperationException();
    }
}

接下來(lái)我們看破壞業(yè)務(wù)正確性的例子涎拉,最典型的例子就是Bob大叔在《敏捷軟件開(kāi)發(fā):原則、模式與實(shí)踐》中講到的正方形繼承矩形的例子了的圆。從一般意義來(lái)看鼓拧,正方形是一種矩形,但這種繼承關(guān)系破壞了業(yè)務(wù)的正確性越妈。

public class Rectangle {
    double width;
    double height;

    public double area() {
        return width * height;
    }
}

public class Square extends Rectangle {
    public void setWidth(double width) {
        this.width = width;
        this.height = width;
    }

    public void setHeight(double height) {
        this.height = width;
        this.width = width;
    }
}

public void testArea(Rectangle r) {
    r.setWidth(5);
    r.setHeight(4);
    assert(r.area() == 20); //! 如果r是一個(gè)正方形季俩,則面積為16
}

代碼中testArea方法的參數(shù)如果是正方形,則面積是16梅掠,而不是期望的20酌住,所以結(jié)果顯然不正確了。

如果你的設(shè)計(jì)滿足里氏替換原則阎抒,那么子類(lèi)(或接口的實(shí)現(xiàn)類(lèi))就可以保證正確性的前提下替換父類(lèi)(或接口)酪我,改變系統(tǒng)的行為,從而實(shí)現(xiàn)擴(kuò)展且叁。BranchByAbstraction絞殺者模式 都是基于里氏替換原則都哭,實(shí)現(xiàn)系統(tǒng)擴(kuò)展和演進(jìn)。這也就是對(duì)修改封閉谴古,對(duì)擴(kuò)展開(kāi)放质涛,因此里氏替換原則是實(shí)現(xiàn)開(kāi)閉原則的一種解決方案稠歉。

而為了達(dá)成里氏替換原則掰担,你需要接口隔離原則。

接口隔離原則 (Interface Segregation Principle)

接口隔離原則說(shuō)的是客戶端不應(yīng)該被迫依賴于它不使用的方法怒炸。簡(jiǎn)單來(lái)說(shuō)就是更小和更具體的瘦接口比龐大臃腫的胖接口好带饱。

胖接口的職責(zé)過(guò)多,很容易違反單一職責(zé)原則阅羹,也會(huì)導(dǎo)致實(shí)現(xiàn)類(lèi)不得不拋出UnsupportedOperationException這樣的異常勺疼,違反里氏替換原則。因此捏鱼,應(yīng)該將接口設(shè)計(jì)得更瘦执庐。

怎么給接口減肥呢?接口之所以存在导梆,是為了解耦轨淌。開(kāi)發(fā)者常常有一個(gè)錯(cuò)誤的認(rèn)知迂烁,以為是實(shí)現(xiàn)類(lèi)需要接口。其實(shí)是消費(fèi)者需要接口递鹉,實(shí)現(xiàn)類(lèi)只是提供服務(wù)盟步,因此應(yīng)該由消費(fèi)者(客戶端)來(lái)定義接口。理解了這一點(diǎn)躏结,才能正確地站在消費(fèi)者的角度定義Role interface却盘,而不是從實(shí)現(xiàn)類(lèi)中提取Header Interface

什么是Role interface? 舉個(gè)例子媳拴,磚頭(Brick)可以被建筑工人用來(lái)蓋房子黄橘,也可以被用來(lái)正當(dāng)防衛(wèi):

public class Brick {
    private int length;
    private int width;
    private int height;
    private int weight;

    public void build() {
        //...包工隊(duì)蓋房
    }

    public void defense() {
        //...正當(dāng)防衛(wèi)
    }
}

如果直接提取以下接口,這就是Header Interface:

public interface BrickInterface {
    void buildHouse();
    void defense();
}

普通大眾需要的是可以防衛(wèi)的武器屈溉,并不需要用磚蓋房子旬陡。當(dāng)普通大眾(Person)被迫依賴了自己不需要的接口方法時(shí),就違反接口隔離原則语婴。正確的做法是站在消費(fèi)者的角度描孟,抽象出Role interface:

public interface BuildHouse {
    void build();
}

public interface StrickCompetence {
    void defense();
}

public class Brick implement BuildHouse, StrickCompetence {
}

有了Role interface,作為消費(fèi)者的普通大眾和建筑工人就可以分別消費(fèi)自己的接口:

Worker.java
brick.build();

Person.java
brick.strike();

接口隔離原則本質(zhì)上也是單一職責(zé)原則的體現(xiàn)砰左,同時(shí)它也服務(wù)于里氏替換原則匿醒。而接下來(lái)介紹的依賴倒置原則可以用來(lái)指導(dǎo)接口隔離原則的實(shí)現(xiàn)。

依賴倒置原則 (Dependence Inversion Principle)

依賴倒置原則說(shuō)的是高層模塊不應(yīng)該依賴底層模塊缠导,兩者都應(yīng)該依賴其抽象廉羔。

這個(gè)原則其實(shí)是在指導(dǎo)如何實(shí)現(xiàn)接口隔離原則,也就是前文提到的僻造,高層的消費(fèi)者不應(yīng)該依賴于具體實(shí)現(xiàn)憋他,應(yīng)該由消費(fèi)者定義并依賴于Role interface,底層的具體實(shí)現(xiàn)也依賴于Role interface髓削,因?yàn)樗獙?shí)現(xiàn)此接口竹挡。

依賴倒置原則是區(qū)分過(guò)程式編程和面向?qū)ο缶幊痰姆炙畮X。過(guò)程式編程的依賴沒(méi)有倒置立膛,A Simple DIP Example | Agile Principles, Patterns, and Practices in C#這篇文章以開(kāi)關(guān)和燈的例子很好地說(shuō)明了這一點(diǎn)揪罕。

上圖的關(guān)系中,當(dāng)Button直接調(diào)用燈的開(kāi)和關(guān)時(shí)宝泵,Button就依賴于燈了好啰。其代碼完全是過(guò)程式編程:

public class Button {   
    private Lamp lamp;   
    public void Poll()   {
        if (/*some condition*/)
           lamp.TurnOn();   
    } 
}

如果Button還想控制電視機(jī),微波爐怎么辦儿奶?應(yīng)對(duì)這種變化的辦法就是抽象框往,抽象出Role interface ButtonServer:

不管是電燈,還是電視機(jī)闯捎,只要實(shí)現(xiàn)了ButtonServer椰弊,Button都可以控制嘁酿。這是面向?qū)ο蟮木幊谭绞健?/p>

總結(jié)

總的來(lái)說(shuō),單獨(dú)應(yīng)用SOLID的某一個(gè)原則并不能讓收益最大化男应。應(yīng)該把它作為一個(gè)整體來(lái)理解和應(yīng)用闹司,從而更好地指導(dǎo)你的軟件設(shè)計(jì)。單一職責(zé)是所有設(shè)計(jì)原則的基礎(chǔ)沐飘,開(kāi)閉原則是設(shè)計(jì)的終極目標(biāo)游桩。里氏替換原則強(qiáng)調(diào)的是子類(lèi)替換父類(lèi)后程序運(yùn)行時(shí)的正確性,它用來(lái)幫助實(shí)現(xiàn)開(kāi)閉原則耐朴。而接口隔離原則用來(lái)幫助實(shí)現(xiàn)里氏替換原則借卧,同時(shí)它也體現(xiàn)了單一職責(zé)。依賴倒置原則是過(guò)程式編程與OO編程的分水嶺筛峭,同時(shí)它也被用來(lái)指導(dǎo)接口隔離原則铐刘。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市影晓,隨后出現(xiàn)的幾起案子镰吵,更是在濱河造成了極大的恐慌,老刑警劉巖挂签,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疤祭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡饵婆,警方通過(guò)查閱死者的電腦和手機(jī)勺馆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)侨核,“玉大人草穆,你說(shuō)我怎么就攤上這事〈暌耄” “怎么了悲柱?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)侥衬。 經(jīng)常有香客問(wèn)我诗祸,道長(zhǎng),這世上最難降的妖魔是什么轴总? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮博个,結(jié)果婚禮上怀樟,老公的妹妹穿的比我還像新娘。我一直安慰自己盆佣,他們只是感情好往堡,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布械荷。 她就那樣靜靜地躺著,像睡著了一般虑灰。 火紅的嫁衣襯著肌膚如雪吨瞎。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,624評(píng)論 1 305
  • 那天穆咐,我揣著相機(jī)與錄音颤诀,去河邊找鬼。 笑死对湃,一個(gè)胖子當(dāng)著我的面吹牛崖叫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拍柒,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼心傀,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了拆讯?” 一聲冷哼從身側(cè)響起脂男,我...
    開(kāi)封第一講書(shū)人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎种呐,沒(méi)想到半個(gè)月后疆液,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡陕贮,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年堕油,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肮之。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡掉缺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出戈擒,到底是詐尸還是另有隱情眶明,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布筐高,位于F島的核電站搜囱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏柑土。R本人自食惡果不足惜蜀肘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望稽屏。 院中可真熱鬧扮宠,春花似錦、人聲如沸狐榔。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)料饥。三九已至索烹,卻和暖如春屠列,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背罢艾。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工楣颠, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人昆婿。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓球碉,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親仓蛆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子睁冬,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 盡管大家都認(rèn)為SOLID是非常重要的設(shè)計(jì)原則能庆,并且對(duì)每一條原則都耳熟能詳施禾,但我發(fā)現(xiàn)大部分開(kāi)發(fā)者并沒(méi)有真正理解。要獲...
    ThoughtWorks閱讀 2,442評(píng)論 4 17
  • 前言 關(guān)于設(shè)計(jì)模式六大設(shè)計(jì)原則的資料網(wǎng)上很多搁胆,但感覺(jué)很多地方解釋地都太過(guò)于籠統(tǒng)化弥搞,特此再總結(jié)一波。 優(yōu)化第一步-單...
    ghroost閱讀 1,106評(píng)論 0 5
  • 單一職責(zé)原則 (SRP) 全稱 SRP , Single Responsibility Principle 單一職...
    米莉_L閱讀 1,765評(píng)論 2 5
  • 設(shè)計(jì)模式概述 在學(xué)習(xí)面向?qū)ο笃叽笤O(shè)計(jì)原則時(shí)需要注意以下幾點(diǎn):a) 高內(nèi)聚渠旁、低耦合和單一職能的“沖突”實(shí)際上攀例,這兩者...
    彥幀閱讀 3,747評(píng)論 0 14
  • 程序設(shè)計(jì)的6大原則: 單一職責(zé)原則里氏替換原則依賴倒置原則接口隔離原則迪米特法則開(kāi)閉原則 從根本學(xué)好,理解為什么要...
    silencefun閱讀 2,408評(píng)論 1 4