用通俗易懂、並且儘量短小精悍的正反例,介紹面向對象SOLID原則。大約15分鐘左右可以消化完成。 ...
面向對象的SOLID原則
簡介
縮寫 | 全稱 | 中文 |
---|---|---|
S | The Single Responsibility Principle | 單一責任原則 |
O | The Open Closed Principle | 開放封閉原則 |
L | Liskov Substitution Principle | 里氏替換原則 |
I | The Interface Segregation Principle | 介面分離原則 |
D | The Dependency Inversion Principle | 依賴倒置原則 |
單一職責原則
一個類只應承擔一種責任。換句話說,讓一個類只做一件事。如果需要承擔更多的工作,那麼分解這個類。
舉例
訂單和賬單上都有流水號、業務時間等欄位。如果只用一個類表達,賦予其雙重職責,後果:
- 特有屬性和共有屬性相互摻雜,難以理解;
- 修改一個場景可能會影響另一個場景。
正確的做法是拆成兩個獨立的類。
開放封閉原則
實體應該對擴展是開放的,對修改是封閉的。即,可擴展(extension),不可修改(modification)。
舉例
一個商戶接入了多個付款方式,支付寶和微信支付,如果將調用支付API的類寫成:
public class PayHandler {
public Result<T> pay(Param param) {
if(param.getType() == "ALIPAY") {
// 支付寶付款調用
...
} else if(param.getType() == "WeChatPay") {
// 微信支付付款調用
...
}
}
}
那麼每次新加一種支付方式,或者修改原有的其中一種支付方式,都要修改PayHandler這個類,可能會影響現有代碼。
比較好的做法是將不同的行為(支付方式)抽象,如下:
public class PayHandler {
private Map<String, PayProcessor> processors;
public Result<T> pay(Param param) {
PayProcessor payProcessor = processors.get(param.getType());
// 異常處理略
return payProcessor.handle(param);
}
}
interface PayProcessor {
Result<T> handle(Param param);
}
public class AlipayProcessor implements PayProcessor {
...
}
public class WeChatPayProcessor implements PayProcessor {
...
}
這樣,新增支付方式只需要新增類,如果使用的是spring等容器,在xml配置對應key-value關係即可;修改已有的支付方式只需要修改對應的類。最大化地避免了對已有實體的修改。
里式替換原則
一個對象在其出現的任何地方,都可以用子類實例做替換,並且不會導致程式的錯誤。換句話說,當子類可以在任意地方替換基類且軟體功能不受影響時,這種繼承關係的建模才是合理的。
舉例
經典的例子: 正方形不是長方形的子類。原因是正方形多了一個屬性“長 == 寬”。這時,對正方形類設置不同的長和寬,計算面積的結果是最後設置那項的平方,而不是長*寬,從而發生了與長方形不一致的行為。如果程式依賴了長方形的面積計算方式,並使用正方形替換了長方形,實際表現與預期不符。
擴展
不能用繼承關係(is-a),但可以用委派關係(has-a)表達。上例中,可以使用正方形類包裝一個長方形類。或者,將正方形和長方形作進一步抽象,使用共有的抽象類。
逸聞
“里氏”指的是芭芭拉·利斯科夫(Barbara Liskov,1939年-),是美國第一個電腦科學女博士,圖靈獎、馮諾依曼獎得主,參與設計並實現了OOP語言CLU,而CLU語言對現代主流語言C++/Java/Python/Ruby/C#都有深遠影響。其項目中提煉出來的數據抽象思想,已成為軟體工程中最重要的精髓之一。(來源: 互動百科)
介面分離原則
客戶(client)不應被強迫依賴它不使用的方法。即,一個類實現的介面中,包含了它不需要的方法。將介面拆分成更小和更具體的介面,有助於解耦,從而更容易重構、更改。
舉例
仍以商家接入移動支付API的場景舉例,支付寶支持收費和退費;微信介面只支持收費。
interface PayChannel {
void charge();
void refund();
}
class AlipayChannel implements PayChannel {
public void charge() {
...
}
public void refund() {
...
}
}
class WeChatChannel implements payChannel {
public void charge() {
...
}
public void refund() {
// 沒有任何代碼
}
}
第二種支付渠道,根本沒有退款的功能,但是由於實現了PayChannel,又不得不將refund()實現成了空方法。那麼,在調用中,這個方法是可以調用的,實際上什麼都沒有做!
改進
將PayChannel拆成各包含一個方法的兩個介面PayableChannel和RefundableChannel。
依賴倒置原則
- 高層次的模塊不應依賴低層次的模塊,他們都應該依賴於抽象。
- 抽象不應依賴於具體實現,具體實現應依賴抽象。
實際上,依賴倒置是實現開閉原則的方法。
舉例
開閉原則的場景仍然可以說明這個問題。以下換一種表現形式。
public class PayHandler {
public Result<T> pay(Param param) {
if(param.getType() == "ALIPAY") {
AlipayProcessor processor = new AlipayProcessor();
processor.hander(param);
...
} else if(param.getType() == "WeChatPay") {
WeChatPayProcessor processor = new WeChatPayProcessor();
processor.hander(param);
...
}
}
}
public class AlipayProcessor { ... }
public class WeChatPayProcessor { ... }
這種實現方式,PayHandler的功能(高層次模塊)依賴了兩個支付Processor(低層次模塊)的實現。
擴展:IOC和DI
控制反轉(IOC)和依賴註入(DI)是Spring中最重要的核心概念之一,而兩者實際上是一體兩面的。
- 依賴註入
- 一個類依賴另一個類的功能,那麼就通過註入,如構造器、setter方法等,將這個類的實例引入。
- 側重於實現。
- 控制反轉
- 創建實例的控制權由一個實例的代碼剝離到IOC容器控制,如xml配置中。
- 側重於原理。
- 反轉了什麼:原先是由類本身去創建另一個類,控制反轉後變成了被動等待這個類的註入。
後記
網路上很多文章中關於SOLID的介紹,語句都不通順,徒增理解難度。如果對基本釋義仍不能領會,可以參考 英文WIKI。