在今天的文章中,我們將更仔細(xì)的討論代碼本身的設(shè)計(jì),特別檢查是否遵循了良好的面向?qū)ο笤O(shè)計(jì)實(shí)踐。和我們已經(jīng)討論過的其他方面一樣,不是所有的團(tuán)隊(duì)都會(huì)將 SOLID 原則列為最重要的檢查項(xiàng),但是如果你在嘗試遵循 SOLID 原則,或者在嘗試將你的代碼往這方面發(fā)展,這里有一些提示可能對(duì)你有幫助。
SOLID 是什么?
SOLID 原則是面向?qū)ο笤O(shè)計(jì)和編程的5個(gè)核心原則。本文的目的不是詳細(xì)講解 SOLID 原則是什么或者深入討論為什么你要遵循這些原則,而是指出在代碼審查中怎么發(fā)現(xiàn)沒有遵循這些原則的味道。
SOLID 代表:
- S - 單一功能原則
- O - 開閉原則
- L - 里氏替換原則
- I - 接口分離原則
- D - 依賴反轉(zhuǎn)原則
單一功能原則(SRP)
在修改一個(gè)類時(shí)永遠(yuǎn)都應(yīng)該只有一個(gè)理由
這一點(diǎn)在單次代碼審查時(shí)可能比較難發(fā)現(xiàn)。根據(jù)這個(gè)規(guī)則的定義,作者在修改代碼是有(或者應(yīng)該有)一個(gè)理由--解決 bug,添加一個(gè)新功能,代碼重構(gòu)。
你需要關(guān)注一個(gè)類里面哪些方法可能會(huì)同時(shí)修改,以及哪些方法不會(huì)因?yàn)槠渌椒ǖ男薷亩薷?。例如?/p>

通過 Upsource 的兩欄差異比較會(huì)發(fā)現(xiàn) TweetMonitor 中添加了一個(gè)新功能,在一些用戶界面的發(fā)帖排行榜繪制前面10個(gè)發(fā)帖者的能力。這看起來是合理的,因?yàn)樗褂昧?onMessage 方法搜集好的數(shù)據(jù),但是有跡象表明它破壞了 SRP 原則。OnMessage 和 getTweetMessageFromFullTweet 方法都是關(guān)于接收并解析一條 Twitter 消息,然而 draw 方法為了UI展示重新獲取相關(guān)數(shù)據(jù)。
代碼審查者應(yīng)該標(biāo)記出這兩個(gè)職責(zé),并且之后和作者一起討論一個(gè)更好的方式來分割這兩個(gè)功能:也許可以將 Twitter 字符串的解析移到一個(gè)不同的類中;或者創(chuàng)建一個(gè)不同的類來負(fù)責(zé)提供發(fā)帖排行榜。
開閉原則(OCP)
軟件實(shí)體(類,模塊,函數(shù)等等)應(yīng)該對(duì)擴(kuò)展開放,但是對(duì)修改封閉。
作為審查者,如果發(fā)現(xiàn)通過一系列的 if 語句來檢查類型,你應(yīng)該意識(shí)到破壞了開閉原則。
如果你在審查上面的代碼,你應(yīng)該很清楚的意識(shí)到,如果一種新的 Event 類型添加到系統(tǒng)中,那么新的類型創(chuàng)建者為了處理新添加的類型,它也許必須添加另一個(gè) else 語句到這個(gè)方法中。
使用多態(tài)來替換這些 if 可能會(huì)好一些:
和往常一樣,這個(gè)問題不止一個(gè)解決方法,但關(guān)鍵是將復(fù)雜的 if/else 和 instanceof 檢查去掉。
里氏替換原則(LSP)
使用了基類引用的函數(shù),在不知道基類子類的情況下,也能夠使用子類的對(duì)象
發(fā)現(xiàn)破壞這一規(guī)則的簡(jiǎn)單方法就是關(guān)注顯式的類型轉(zhuǎn)換。如果你必須將一個(gè)對(duì)象轉(zhuǎn)換為其他類型,那么你并沒有“在不知道子類信息的情況下”使用基類。
在檢查 LSP 的以下兩個(gè)條件時(shí),會(huì)發(fā)現(xiàn)更多微妙的破壞 LSP:
- (當(dāng)子類的方法重載父類的方法時(shí))方法的前置條件(即方法的形參)要比父類方法的輸入?yún)?shù)更寬松。
- (當(dāng)子類的方法實(shí)現(xiàn)父類的抽象方法時(shí))方法的后置條件(即方法的返回值)要比父類更嚴(yán)格。
想象一下,例如我們有一個(gè)抽象類 Order,它有一系列子類 - BookOrder,ElectronicsOrder 等等。Order 類的
PlaceOrder 方法接收 Warehouse 參數(shù),并以此修改倉庫中的庫存水平:
現(xiàn)在假設(shè)我們引入了新的電子禮品卡,這個(gè)只需要往錢包里添加余額就可以,不需要實(shí)際的庫存。如果用 GiftCardOrder 類來實(shí)現(xiàn)電子禮品卡,placeOrder 方法就不必使用 warehouse 參數(shù):
這看起來像是合理的使用繼承,但事實(shí)上你是希望使用 GiftCardOrder 類的代碼能夠像使用其他類那樣使用它,即你希望所有的子類都能通過測(cè)試:
但是這個(gè)測(cè)試并通不過,因?yàn)?GiftCardOrder 有不同的訂購(gòu)行為。如果你在審查這一類代碼,確認(rèn)這里使用繼承是否合理--也許訂購(gòu)行為可以通過組合而不是繼承來插入。
接口分離原則(ISP)
多個(gè)明確的客戶端接口要好于一個(gè)通用的接口
如果代碼中有接口定義了很多個(gè)方法,那么很容易確認(rèn)它破壞了這一規(guī)則。這一條規(guī)則和 SRP 是一致的,你可能會(huì)發(fā)現(xiàn)擁有多個(gè)方法的接口實(shí)際上會(huì)負(fù)責(zé)多個(gè)方面或者功能。
但是有時(shí)只有兩個(gè)方法的接口也應(yīng)該分為兩個(gè)接口:
在這個(gè)例子中,假設(shè)有時(shí)候不需要 decode 方法,并且某一個(gè) codec 在不同的場(chǎng)合有可能當(dāng)做 endoder 使用,有時(shí)可能當(dāng)做 decoder 使用,那么把 SimpleCodec 拆分成 Encoder 和 Decoder 更合適一些。有的類可能會(huì)同時(shí)實(shí)現(xiàn)這兩個(gè)接口,但是不必讓所有的實(shí)現(xiàn)者都去 Override 它們不需要的方法,或者說只需要 Encoder 接口的類注意到它們的 Encoder 實(shí)例還實(shí)現(xiàn)了 decode。
依賴反轉(zhuǎn)原則(DIP)
依賴于抽象,而不是具體的實(shí)現(xiàn)。
發(fā)現(xiàn)簡(jiǎn)單的破壞這一規(guī)則可能比較容易,比如使用 new 關(guān)鍵字(而不是使用依賴注入或者工廠模式)或者對(duì)你的集合類型過度熟悉(例如將變量和參數(shù)定義為 ArrayList 而不是 List),作為審查者,你應(yīng)該注意保證代碼作者使用/創(chuàng)建了正確的抽象。
例如,服務(wù)級(jí)別的代碼使用直接和數(shù)據(jù)庫之間的連接來讀寫數(shù)據(jù):

這段代碼依賴于許多具體的實(shí)現(xiàn)細(xì)節(jié):數(shù)據(jù)庫連接 JDBC,數(shù)據(jù)庫特定的 SQL,數(shù)據(jù)庫的結(jié)構(gòu)等等。這些代碼應(yīng)該出現(xiàn)在系統(tǒng)的某一個(gè)地方,但是不應(yīng)該出現(xiàn)在這里,也不應(yīng)該出現(xiàn)在不需要了解數(shù)據(jù)庫細(xì)節(jié)的方法中。更好的方法是提取出一個(gè) DAO 或者使用 Repository 模式,然后將 DAO 或者 repository 注入到這個(gè) services。
總結(jié)
這些代碼“味道”可能表示一個(gè)或者多個(gè) SOLID 原則被破壞:
- 很長(zhǎng)的
if/else語句 - 強(qiáng)制轉(zhuǎn)換到子類型
- 很多公共方法
- 實(shí)現(xiàn)了拋出
UnsupportedOperationException的方法
與所有設(shè)計(jì)問題一樣,在遵循這些原則之間找到平衡,并根據(jù)你的團(tuán)隊(duì)的喜好做出調(diào)整。 但是,如果在代碼審查中看到復(fù)雜的代碼,你可能會(huì)發(fā)現(xiàn)應(yīng)用這些原則之一會(huì)找到一個(gè)更簡(jiǎn)單,更易于理解的解決方案。