物件導向設計原則和建立SOLID應用的5個方法
最近我聽到了很多關於函數語言程式設計(FP),受之啟發我覺得也應該關注物件導向程式設計(OOP)和麵向物件設計(OOD),因為在設計系統時這些仍然非常重要。
我們將以SOLID原則為起點開始我們的旅程。SOLID原則是類級別的,物件導向的設計理念,它們與測試工具一起幫你改進腐壞的程式碼。SOLID由程式設計師們最喜歡的大叔 Robert C. Martin(Bob大叔)提出,它其實是五個其他縮略詞的組合——SRP, OCP, LSP, ISP, DIP,我會在下面有更深入的介紹。最重要的是,SOLID原則使你的軟體變得更有價值。
呃,這個程式碼有壞味道…
程式碼腐壞
1.當應用程式程式碼大量腐壞,開發人員會發現程式碼越來越難以維護、臃腫。那麼如何識別將來的程式碼腐壞?這些跡象可能表明將要程式碼腐壞:
-
僵化——小的變化導致整個系統重建。
-
脆弱——一個模組的變化導致其他不相關模組不正常執行。想象一個汽車系統,改變電臺的功能會影響到窗戶的使用。
-
固定——一個模組的內部元件不能被抽取並且在新環境重用。比如一個應用程式的登入模組不能在完全不同的系統中使用,那麼這個模組是固定的,這是由於各模組之間的耦合和依賴造成的。改進的策略是從低層次的細節,比如特定的資料庫,UI實現(Web,桌面),特殊框架等解耦核心抽象。
-
粘性——程式碼構建和測試很難執行,並且要花費很長時間執行,甚至小的變化有很高的成本,並且要求在多個位置/層次修改。
使用者期望從他們所用的軟體之外得到一些價值。一個應用程式的價值在於它能否幫助使用者將一些事情做得更好,增加生產力或者時間或金錢,在“浪費”上有所節省。人們通常付出金錢來換取價值高的軟體。
但是使用者從偉大的軟體得到了次要價值。我想要談談這個價值,因為這也是人們在談論軟體價值時最先想到的:功能。
如果軟體完成了使用者需求的同時沒有崩潰和延遲,那麼這個軟體的次要價值就高。軟體滿足了使用者的當前需求,使用者就獲得了次要價值。但是,使用者需求經常變化,軟體提供的功能和使用者需求很容易不同步,這導致了價值降低。為了保持次要價值高,軟體必須能夠跟上使用者不斷變化的需求。所以在這裡我們來談談軟體的首要價值,它必須能夠容忍和有助於正在進行的變化。
試想一下,你的軟體目前可以滿足使用者的需求,但是實在是很難改變,改變成本很高。那麼,由於應用程式的不靈活性以及其盈利能力可能降低,使用者會不高興。
現在試想其他的軟體開始時次要價值低,但是它可以容易且廉價地改變。盈利能力持續上升,使用者也越來越高興。
那麼什麼是SOLID原則?
單一職責原則(SRP)
單一職責原則(Single Responsibility Principle,SRP)指出,一個類發生變化的原因不應該超過一個。這意味著程式碼中每個類,或者類似的結構只有一個功能。
在類中的一切都與該單一目的有關,即內聚性。這並不是說類只應該含有一個方法或屬性。
類中可以包括很多成員,只要它們與單一的職責有關。當類改變的一個原因出現時,類的多個成員可能多需要修改。也可能多個類將需要更新。
下面的程式碼有多少職責?
class Employee { public Pay calculatePay() {...} public void save() {...} public String describeEmployee() {...} }
正確答案是3個。
在一個類中混合了1)支付的計算邏輯,2)資料庫邏輯,3)描述邏輯。如果你將多個職責結合在一個類中,可能很難實現修改一部分時不會破壞其他部分。混合職責也使這個類難以理解,測試,降低了內聚性。修改它的最簡單方法是將這個類分割為三個不同的相互分離的類,每個類僅僅有一個職責:資料庫訪問,支付計算和描述。
開閉原則(OCP)
開閉原則(Open-Closed Principle,OCP)指出:類應該對擴充套件開放但對修改關閉。“對擴充套件開放”指的是設計類時要考慮到新需求提出時類可以增加新的功能。“對修改關閉”指的是一旦一個類開發完成,除了改正bug就不再修改它。
這個原則的兩個部分似乎是對立的。但是,如果正確地設計類和他們的依賴關係,就可以增加功能而不修改已有的原始碼。
通常來說可以通過依賴關係的抽象實現開閉原則,比如介面或抽象類而不是具體類。通過建立新的類實現介面來增加功能。
在專案中應用OCP原則可以限制程式碼的更改,一旦程式碼完成,測試和除錯之後就很少再去更改。這減少了給現有程式碼引入新bug的風險,增強軟體的靈活性。
為依賴關係使用介面的另一個作用是減少耦合和增加靈活性。
void checkOut(Receipt receipt) { Money total = Money.zero; for (item : items) { total += item.getPrice(); receipt.addItem(item); } Payment p = acceptCash(total); receipt.addPayment(p); }
那麼增加信用卡支援該怎麼做?你可能像下面的增加if語句,但這違反OCP原則。
Payment p; if (credit) p = acceptCredit(total); else p = acceptCash(total); receipt.addPayment(p);
更好的解決方案是:
public interface PaymentMethod {void acceptPayment(Money total);} void checkOut(Receipt receipt, PaymentMethod pm) { Money total = Money.zero; for (item : items) { total += item.getPrice(); receipt.addItem(item); } Payment p = pm.acceptPayment(total); receipt.addPayment(p); }
這兒有一個小祕密:OCP僅僅用於即將到來的變化可預見的情況,那麼只有類似的變化已經發生時應用它。所以,首先做最簡單的事情,然後判斷會有什麼變化,就能更加準確地預見將來的變化。
這意味著等待使用者做出改變,然後使用抽象應對將來的類似變化。
里氏替換原則(LSP)
里氏替換原則(Liskov Substitution Principle,LSP)適用於繼承層次結構,指出設計類時客戶端依賴的父類可以被子類替代,而客戶端無須瞭解這個變化。
因此,所有的子類必須按照和他們父類相同方式操作。子類的特定功能可能不同,但是必須符合父類的預期行為。要成為真正的行為子型別,子類必須不僅要實現父類的方法和屬性,也要符合其隱含行為。
一般來說,如果父型別的一個子型別做了一些父型別的客戶沒有預期的事情,那這就違反LSP。比如一個派生類丟擲了父類沒有丟擲的異常,或者派生類有些不能預期的副作用。基本上派生類永遠不應該比父類做更少的事情。
一個違反LSP的典型例子是Square類派生於Rectangle類。Square類總是假定寬度與高度相等。如果一個正方形物件用於期望一個長方形的上下文中,可能會出現意外行為,因為一個正方形的寬高不能(或者說不應該)被獨立修改。
解決這個問題並不容易:如果修改Square類的setter方法,使它們保持正方形不變(即保持寬高相等),那麼這些方法將弱化(違反)Rectangle類setter方法,在長方形中寬高可以單獨修改。
public class Rectangle { private double height; private double width; public double area(); public void setHeight(double height); public void setWidth(double width); }
以上程式碼違反了LSP。
public class Square extends Rectangle { public void setHeight(double height) { super.setHeight(height); super.setWidth(height); } public void setWidth(double width) { setHeight(width); } }
違反LSP導致不明確的行為。不明確的行為意味著它在開發過程中執行良好但在產品中出現問題,或者要花費幾個星期除錯每天只出現一次的bug,或者不得不查閱數百兆日誌找出什麼地方發生錯誤。
介面隔離原則(ISP)
介面隔離原則(Interface Segregation Principle)指出客戶不應該被強迫依賴於他們不使用的介面。當我們使用非內聚的介面時,ISP指導我們建立多個較小的內聚度高的介面。
當你應用ISP時,類和他們的依賴使用緊密集中的介面通訊,最大限度地減少了對未使用成員的依賴,並相應地降低耦合度。小介面更容易實現,提升了靈活性和重用的可能性。由於很少的類共享這些介面,為響應介面的變化而需要變化的類數量降低,增加了魯棒性。
基本上,這裡的教訓是“不要依賴你不需要的東西”。下面是例子:
想象一個ATM取款機,通過一個螢幕顯示我們想要的不同資訊。你會如何解決顯示不同資訊的問題?我們使用SRP,OCP和LSP想出一個方案,但是這個系統仍然很難維護。這是為什麼?
想象ATM的所有者想要新增僅在取款功能出現的一條資訊,“ATM機將在您取款時收取一些費用,您同意嗎”。你會如何解決?
可能你會給Messenger介面增加一個方法並使用這個方法完成。但是這會導致重新編譯這個介面的所有使用者,幾乎所有的系統需要重新部署,這直接違反了OCP。讓程式碼腐壞開始了!
這裡出現了這樣的情形:對於取款功能的改變導致其他全部非相關功能也變化,我們現在知道這並不是我們想要的。這是怎麼回事?
其實,這裡是向後依賴在作怪,使用了該Messenger介面每個功能依賴了它不需要,但是被其他功能需要的方法,這正是我們想要避免的。
public interface Messenger { askForCard(); tellInvalidCard(); askForPin(); tellInvalidPin(); tellCardWasSiezed(); askForAccount(); tellNotEnoughMoneyInAccount(); tellAmountDeposited(); tellBalance(); }
相反,將Messenger介面分割,不同的ATM功能依賴於分離的Messenger。
public interface LoginMessenger { askForCard(); tellInvalidCard(); askForPin(); tellInvalidPin(); } public interface WithdrawalMessenger { tellNotEnoughMoneyInAccount(); askForFeeConfirmation(); } publc class EnglishMessenger implements LoginMessenger, WithdrawalMessenger { ... }
依賴反轉原則(DIP)
依賴反轉原則(Dependency Inversion Principle,DIP)指出高層次模組不應該依賴於低層次模組;他們應該依賴於抽象。第二,抽象不應該依賴於細節;細節依賴於抽象。方法是將類孤立在依賴於抽象形成的邊界後面。如果在那些抽象後面所有的細節發生變化,那我們的類仍然安全。這有助於保持低耦合,使設計更容易改變。DIP也允許我們做單獨測試,比如作為系統外掛的資料庫等細節。
例子:一個程式依賴於Reader和Writer介面,Keyboard和Printer作為依賴於這些抽象的細節實現了這些介面。CharCopier是依賴於Reader和Writer實現類的低層細節,可以傳入任何實現了Reader和Writer介面的裝置正確地工作。
public interface Reader { char getchar(); } public interface Writer { void putchar(char c)} class CharCopier { void copy(Reader reader, Writer writer) { int c; while ((c = reader.getchar()) != EOF) { writer.putchar(); } } } public Keyboard implements Reader {...} public Printer implements Writer {...}
最後的問題——使用SOLID
我想SOLID原則是你的工具箱裡很有價值的工具。在設計下一個功能或者應用時他們就應該在你的腦海中。正如Bob大叔在他那不朽的帖子中總結的:
SRP | 單一職責原則 | 一個類有且只有一個更改的原因。 |
OCP | 開閉原則 | 能夠不更改類而擴充套件類的行為。 |
LSP | 里氏替換原則 | 派生類可以替換基類被使用。 |
ISP | 介面隔離原則 | 使用客戶端特定的細粒度介面。 |
DIP | 依賴反轉原則 | 依賴抽象而不是具體實現。 |
而且,將這些原則應用在專案中。
原文連結: zeroturnaround 翻譯: ImportNew - 何 佳妮
相關文章
- SOLID:物件導向設計的前五項原則Solid物件
- SOLID:物件導向設計的五個基本原則Solid物件
- 不止於物件導向的SOLID原則物件Solid
- 物件導向設計的六大原則(SOLID原則)-——里氏替換原則物件Solid
- 物件導向設計原則和模式物件模式
- 物件導向的設計原則物件
- 物件導向設計原則物件
- 物件導向設計原則之合成複用原則物件
- 物件導向設計原則,以及包的設計原則物件
- 物件導向的基本設計原則物件
- 程式設計師應當知道的10個物件導向設計原則程式設計師物件
- 物件導向設計原則概述物件
- 物件導向設計(OOD)原則物件
- Java程式設計師應當知道的10個物件導向設計原則Java程式設計師物件
- Java程式設計師應瞭解的10個物件導向設計原則Java程式設計師物件
- 開閉原則——物件導向程式設計原則物件程式設計
- 物件導向設計原則之開閉原則物件
- Java中物件導向的設計原則Java物件
- 物件導向的編碼設計原則物件
- The Principles of OOD 物件導向設計原則物件
- 2.物件導向設計原則物件
- 物件導向之旅-設計與設計原則物件
- 物件導向設計原則之介面隔離原則物件
- 物件導向設計原則之里氏代換原則物件
- 【設計原則】物件導向程式設計的六大原則物件程式設計
- 物件導向設計的6大原則物件
- 物件導向設計6大原則物件
- 物件導向設計原則&設計模式分類物件設計模式
- 前端設計模式(0)物件導向&&設計原則前端設計模式物件
- 物件導向設計原則之單一職責原則物件
- 61條物件導向設計的經驗原則物件
- 七種常見的物件導向設計原則物件
- 我學設計模式 之 物件導向設計原則設計模式物件
- 翻譯 | The Principles of OOD 物件導向設計原則物件
- 物件導向之六大設計原則物件
- 物件導向設計原則之迪米特法則物件
- 程式設計師應知道這十大物件導向設計原則程式設計師物件
- 物件導向設計的幾個基本原則物件