面向?qū)ο蟮幕?/h2>
從事軟件開發(fā)的朋友或多或少都聽過以下一些原則:比如KiSS鹤盒、DRY、LKP侦副、COC侦锯、DbC、SoC秦驯、HP尺碰、SOLID等。這些原則已經(jīng)在業(yè)界被證實了自身的價值译隘,尤其當談到面向?qū)ο笤O計的時候亲桥,SOLID則是一個避不開的主題。
作為面向?qū)ο蟮幕驹瓌t固耘,SOLID本身就是一個明顯的招牌 - 堅固的磐石锨亏,撐起了面向?qū)ο笤O計大廈捉捅。
SOLID由五大原則構成:
- Single Responsibility Principle【單一職責原則】
- Open Close Principle【開閉原則】
- Liskov Substitution Principle【里氏替換原則】
- Interface Segregation Principle【接口隔離原則】
- 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ā)生了改變。
看看Rectangle
和Square
的實現(xiàn)剩蟀,不難發(fā)現(xiàn)子類Square
重寫了setHeight
和setWidth
方法催蝗,修改了父類行為,導致了替換失敗育特。
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
類焚碌,首先對自身屬性width
和height
進行了隱藏畦攘,通過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
繼承自Rectangle
,Square
能夠復用Rectangle
中的所有行為身腻,假如你不對Square
做任何事情就能完美復用产还,但這樣子出來的正方形可能寬和高就不一樣了(無法滿足客戶真實需求,這可都是無用功喲)嘀趟。為了滿足客戶需求,你就不得不對setWidth
和setHeight
進行重寫:
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ī)矩又是什么呢陵刹?此時你必須重寫setWidth
和setHeight
默伍,畢竟?jié)M足客戶才是你首要目的。到這個時候衰琐,已經(jīng)說明了該繼承關系出了點問題也糊。你需要做的是跳出來,重新審視一下你的設計:
Square
和Rectangle
都有寬和高羡宙,并且計算面積的方式一樣狸剃,不同的是setWidth
和setHeight
。是否可以將共同的特征進一步抽象提煉狗热。就這樣逼著自己去思考钞馁,你可能很快就抽象出一個四邊形,因為setWidth
和setHeight
行為不確定匿刮,先將它們抽象化僧凰。
你很快用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; }
}
然后你讓Rectangle
和Square
分別繼承自Quads
,各自在自己的類中實現(xiàn)setWidth
和setHeight
熟丸。這時候你使用Rectangle
和Square
的方式也改了:
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