讓里氏替換原則為你效力

面向?qū)ο蟮幕?/h2>

從事軟件開發(fā)的朋友或多或少都聽過以下一些原則:比如KiSS鹤盒、DRY、LKP侦副、COC侦锯、DbC、SoC秦驯、HP尺碰、SOLID等。這些原則已經(jīng)在業(yè)界被證實了自身的價值译隘,尤其當談到面向?qū)ο笤O計的時候亲桥,SOLID則是一個避不開的主題。

作為面向?qū)ο蟮幕驹瓌t固耘,SOLID本身就是一個明顯的招牌 - 堅固的磐石锨亏,撐起了面向?qū)ο笤O計大廈捉捅。

SOLID由五大原則構成:

  1. Single Responsibility Principle【單一職責原則】
  2. Open Close Principle【開閉原則】
  3. Liskov Substitution Principle【里氏替換原則】
  4. Interface Segregation Principle【接口隔離原則】
  5. Dependency Inversion Principle【依賴倒置原則】

對于大部分OO程序員练湿,這五大原則的名字可能已經(jīng)耳熟能詳忽肛,卻總不能很清晰的描述出SOLID是如何為我們服務,因為SOLID從來也沒有告訴我們How损敷,它只在說:"這就是你最終要達到的目的地"葫笼。

本文我將帶著我的思考來捋一下LSP,LSP可能是一個很容易被破壞的原則拗馒,理解了它將能夠很好地驅(qū)動我們?nèi)ニ伎既绾握_地做抽象設計路星。


打破里氏替換原則

In a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program (correctness, task performed, etc.) -- Barbara Liskov in 1987

簡單描述LSP:一個子類實例對象替換掉其父類實例對象,不會引發(fā)程序的任何變化诱桂。

如果要保證這點洋丐,我們在設計類的繼承關系的時候,子類不應重寫父類的方法访诱,這樣保證了父類的行為沒有被修改垫挨。來看個代碼示例韩肝,一個Square類繼承自Rectangle類触菜,我們計算它們的面積:


class RectangleTest {
    @Test
    void should_return_area_when_calculate_given_width_and_height_valid() {
        Rectangle rectangle = new Rectangle();
        rectangle.setHeight(3);
        rectangle.setWidth(5);

        assertThat(rectangle.calculateArea()).isEqualTo(15);
    }
    @Test
    void should_return_area_when_calculate_given_width_and_height_valid() {
        Rectangle rectangle = new Square();
        rectangle.setHeight(3);
        rectangle.setWidth(5);

        assertThat(rectangle.calculateArea()).isEqualTo(25);
    }
}

前者返回的是15,而后者返回的是25哀峻。這兩者的差別在于我們使用了Square替換掉Rectangle涡相,從而導致了程序的行為發(fā)生了改變。

看看RectangleSquare的實現(xiàn)剩蟀,不難發(fā)現(xiàn)子類Square重寫了setHeightsetWidth方法催蝗,修改了父類行為,導致了替換失敗育特。

public class Rectangle {
    protected double width;
    protected double height;
    public void setWidth(double width) { this.width = width; }
    public void setHeight(double height) { this.height = height; }
    public double calculateArea() { return width * height; }
}

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

所以按照LSP的觀點丙号,這個繼承關系被扣上不良的帽子先朦,注意這里我用了不良,而非錯誤犬缨。因為我知道你可能會懟我:"我設計這個子類我就想基于父類做一個擴展喳魏,不同的子類有不同的實現(xiàn),沒讓你在使用的時候去替換父類呀! " (我敢打賭你在項目中遇到過這種覆寫父類的實現(xiàn)怀薛,并且軟件還能正常Work)刺彩。在我懟回去之前,請先跟我來回顧一下面向?qū)ο筇匦浴?/p>


從復用來看繼承

從一踏入職場那一刻枝恋,我就在面試中多次被問過:請談談你對面向?qū)ο蟮娜筇匦缘睦斫猓?/p>

簡單捋一下面向?qū)ο蟮娜筇匦裕?/p>

  • 封裝:隱藏對象的屬性和實現(xiàn)細節(jié)创倔,僅對外公開接口。比如Rectangle類焚碌,首先對自身屬性widthheight進行了隱藏畦攘,通過calculateArea方法提供服務,將依賴自身數(shù)據(jù)的計算細節(jié)也進行了隱藏
  • 繼承:允許子類在不需要重新編寫父類的前提下十电,復用父類的所有功能念搬,并能夠按需進行擴展。比如Square繼承了Rectangle摆出,就具有了calculateArea的功能朗徊。(當心:這個繼承不合理)
  • 多態(tài):允許對象在運行期表現(xiàn)出不同的形體

恰巧LSP中提到了子類和父類的概念,所以不得不說說繼承偎漫。在這之前爷恳,我假設:在做面向?qū)ο筌浖O計的你認同面向?qū)ο笤O計的價值 -- 提升軟件的對變化的響應力

繼承的最核心的目的之一是為了復用象踊,很多時候我們?yōu)榱藦陀貌捎昧死^承温亲。如果我們設計繼承單純?yōu)榱藦陀茫憧赡軙枮樯恫挥媒M合杯矩?而且很多時候提倡組合優(yōu)于繼承栈虚。這就需要我們思考面向?qū)ο蟮脑O計初衷:面向?qū)ο蠼⒃趯φ鎸嵤澜绲某橄笄疤嵘希艽蟪潭壬戏从沉宋覀兊恼鎸嵤澜缡仿 1热缫恢畸W鵡是一只鳥魂务,鳥能飛,鸚鵡也能飛泌射,所以讓鸚鵡繼承自鳥粘姜,鸚鵡具備了飛的能力。

public class Bird {
    public void fly() {
        System.out.println("I am flying");
    }
}

public class Parrot extends Bird {}

如何決定繼承關系熔酷,你可以用Is-A來進行初步驗證孤紧,比如A parrot is a bird。當你使用Is-A讀起來就能讓自己發(fā)笑得的時候拒秘,就說明這個繼承就明顯不合理了号显。比如臭猜,你有一架飛機,它也能飛押蚤,為了復用你讓飛機繼承鳥 -- A Plane is a Bird获讳。當關系不是那么明顯的時候怎么辦?比如活喊,A Square is a Rectangle丐膝,下文我將給出答案。

所以钾菊,繼承首先它應該體現(xiàn)一種現(xiàn)實世界的真實規(guī)則Is-A帅矗,復用是它提供的一個核心能力,也是我們期望在設計上能獲得的好處煞烫,而要達到復用浑此,就要遵守一個規(guī)則:子類不去更改父類已有的行為,否則就與復用不沾邊了(復用滞详,代表你啥也不用做凛俱,直接具備的行為,如果你重新實現(xiàn)了料饥,那叫新的實現(xiàn)蒲犬,你付出了新的努力。至于你要添加新的行為岸啡,這屬于擴展原叮,按需就好)。

我想你已經(jīng)能夠運用Is-A來避免很多明顯恰當?shù)睦^承關系巡蘸。而當你面臨模棱兩可的繼承場景時奋隶,從復用的視角出發(fā),LSP提供了很好的校驗規(guī)則悦荒。


抽象是為了更好地復用

回到文章一開始的例子唯欣,使用Is-A來解讀:A Square is a Rectangle,好像還湊合搬味,但有點把握不準境氢。

Square繼承自RectangleSquare能夠復用Rectangle中的所有行為身腻,假如你不對Square做任何事情就能完美復用产还,但這樣子出來的正方形可能寬和高就不一樣了(無法滿足客戶真實需求,這可都是無用功喲)嘀趟。為了滿足客戶需求,你就不得不對setWidthsetHeight進行重寫:

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

一旦重寫愈诚,當你將下面代碼的Rectangle替換成Square時候就掛了:

@Test
void should_return_area_when_calculate_given_width_and_height_valid() {
    // Replace with Square
    Rectangle rectangle = new Rectangle(); 
    rectangle.setHeight(3);
    rectangle.setWidth(5);

    // 25 if using Square
    assertThat(rectangle.calculateArea()).isEqualTo(15); 
}

所以她按,Liskov就開始吶喊了:"說好的Square只是復用Rectangle的呢牛隅,為啥把Rectangle的行為改了,程序掛了酌泰,你不守規(guī)矩媒佣,怎么回事!"

那規(guī)矩又是什么呢陵刹?此時你必須重寫setWidthsetHeight默伍,畢竟?jié)M足客戶才是你首要目的。到這個時候衰琐,已經(jīng)說明了該繼承關系出了點問題也糊。你需要做的是跳出來,重新審視一下你的設計:

SquareRectangle都有寬和高羡宙,并且計算面積的方式一樣狸剃,不同的是setWidthsetHeight。是否可以將共同的特征進一步抽象提煉狗热。就這樣逼著自己去思考钞馁,你可能很快就抽象出一個四邊形,因為setWidthsetHeight行為不確定匿刮,先將它們抽象化僧凰。

你很快用Java代碼實現(xiàn):

public abstract class Quads {
    protected int width;
    protected int height;
    
    public abstract void setWidth(int width);
    public abstract void setHeight(int height);
    
    public int calculateArea() { return width * height; }
}

然后你讓RectangleSquare分別繼承自Quads,各自在自己的類中實現(xiàn)setWidthsetHeight熟丸。這時候你使用RectangleSquare的方式也改了:

class QuadsTest {
    @Test
    void should_return_area_when_calculate_given_width_and_height_valid() {
        Quads quads = new Rectangle();
        quads.setHeight(3);
        quads.setWidth(5);

        assertThat(quads.calculateArea()).isEqualTo(15);
    }
    @Test
    void should_return_area_when_calculate_given_width_and_height_valid() {
        Quads quads = new Square();
        quads.setWidth(5);
        assertThat(quads.calculateArea()).isEqualTo(25);
    }
}

通過進一步抽象允悦,你改變了繼承關系,Rectangle is A Quads虑啤,Square is A Quads隙弛,此時這種繼承關系就更加清楚明顯了,并且沒有違背LSP(Quads是一個抽象類狞山,不能實例化對象全闷,所以不會出現(xiàn)子類實例對象替換父類實例對象的場景)。

到這里萍启,你已經(jīng)成功通過了進一步抽象拯救了這個繼承關系总珠,而且新的繼承關系更加合理,更加符合面向?qū)ο笤O計勘纯,也最大化發(fā)揮了繼承的核心能力 -- 復用局服。

很多時候我們遇到這種,可能是因為我們過于著急寫代碼或是疏忽大意驳遵,那些貌似像Is-A的關系也被我們用上了繼承淫奔,這也促成了繼承被濫用。而解決辦法也很簡單堤结,LSP這個工具提供了很大的幫助唆迁,最終你會發(fā)現(xiàn)大多是由于恰當抽象的缺失鸭丛。


總結(jié)

如果用一句話來形容LSP,我覺得是:當你無法根據(jù)Is-A 來判斷繼承關系是否合理時唐责,你應該思考如何進行下一步抽象鳞溉,從而避免讓繼承產(chǎn)生二義性

借用極限編程的理念來講:將我們認同的有效軟件開發(fā)原理和實踐應用到極限。我們在做面向?qū)ο笤O計時鼠哥,不妨拿起LSP這個現(xiàn)成的工具熟菲,幫助我們有效地減少繼承的濫用、模糊意圖等設計缺陷朴恳,提升軟件設計抄罕。

當然,很多不符合LSP的軟件也能工作菜皂,這就像很多軟件充斥著壞味道照樣能工作一樣(比如代碼注釋)贞绵。而它背后隱含的邏輯應該是:

我們應該積極去思考更好的設計,而不是過早放棄思考的機會


注釋

  • KiSS: Keep it simple, stupid
  • DRY: Don't Repeat Yourself
  • LKP: Least Knowledge Principle (LOD: Law of Demeter)
  • CoC: Convention over Configuration
  • DbC: Design by Contract
  • SoC: Segregation of Concerns
  • HP: Hollywood Principle

參考閱讀


Posted by 袁慎建@ThoughtWorks

版權聲明:自由轉(zhuǎn)載?非商用?非衍生?保持署名 | Creative Commons BY-NC-ND 4.0

原文鏈接:https://sjyuan.cc/make-lsp-working-for-you/

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市章母,隨后出現(xiàn)的幾起案子母蛛,更是在濱河造成了極大的恐慌,老刑警劉巖乳怎,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件彩郊,死亡現(xiàn)場離奇詭異,居然都是意外死亡蚪缀,警方通過查閱死者的電腦和手機秫逝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來询枚,“玉大人违帆,你說我怎么就攤上這事〗鹗瘢” “怎么了刷后?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長渊抄。 經(jīng)常有香客問我尝胆,道長,這世上最難降的妖魔是什么护桦? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任含衔,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘抱慌。我一直安慰自己逊桦,他們只是感情好眨猎,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布抑进。 她就那樣靜靜地躺著,像睡著了一般睡陪。 火紅的嫁衣襯著肌膚如雪寺渗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天兰迫,我揣著相機與錄音信殊,去河邊找鬼。 笑死汁果,一個胖子當著我的面吹牛涡拘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播据德,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼鳄乏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了棘利?” 一聲冷哼從身側(cè)響起橱野,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎善玫,沒想到半個月后水援,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡茅郎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年蜗元,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片系冗。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡奕扣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出毕谴,到底是詐尸還是另有隱情成畦,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布涝开,位于F島的核電站循帐,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏舀武。R本人自食惡果不足惜拄养,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瘪匿,春花似錦跛梗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至顽染,卻和暖如春漾岳,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背粉寞。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工尼荆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人唧垦。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓捅儒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親振亮。 傳聞我的和親對象是個殘疾皇子巧还,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359

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