在今天的文章中址芯,我們將更仔細的討論代碼本身的設(shè)計,特別檢查是否遵循了良好的面向?qū)ο笤O(shè)計實踐瓢姻。和我們已經(jīng)討論過的其他方面一樣墩莫,不是所有的團隊都會將 SOLID 原則列為最重要的檢查項,但是如果你在嘗試遵循 SOLID 原則羞福,或者在嘗試將你的代碼往這方面發(fā)展惕蹄,這里有一些提示可能對你有幫助。
SOLID 是什么治专?
SOLID 原則是面向?qū)ο笤O(shè)計和編程的5個核心原則卖陵。本文的目的不是詳細講解 SOLID 原則是什么或者深入討論為什么你要遵循這些原則,而是指出在代碼審查中怎么發(fā)現(xiàn)沒有遵循這些原則的味道张峰。
SOLID 代表:
- S - 單一功能原則
- O - 開閉原則
- L - 里氏替換原則
- I - 接口分離原則
- D - 依賴反轉(zhuǎn)原則
單一功能原則(SRP)
在修改一個類時永遠都應(yīng)該只有一個理由
這一點在單次代碼審查時可能比較難發(fā)現(xiàn)泪蔫。根據(jù)這個規(guī)則的定義,作者在修改代碼是有(或者應(yīng)該有)一個理由--解決 bug喘批,添加一個新功能撩荣,代碼重構(gòu)。
你需要關(guān)注一個類里面哪些方法可能會同時修改谤祖,以及哪些方法不會因為其他方法的修改而修改婿滓。例如:
通過 Upsource 的兩欄差異比較會發(fā)現(xiàn) TweetMonitor
中添加了一個新功能,在一些用戶界面的發(fā)帖排行榜繪制前面10個發(fā)帖者的能力粥喜。這看起來是合理的凸主,因為它使用了 onMessage
方法搜集好的數(shù)據(jù),但是有跡象表明它破壞了 SRP 原則额湘。OnMessage
和 getTweetMessageFromFullTweet
方法都是關(guān)于接收并解析一條 Twitter 消息卿吐,然而 draw
方法為了UI展示重新獲取相關(guān)數(shù)據(jù)旁舰。
代碼審查者應(yīng)該標記出這兩個職責,并且之后和作者一起討論一個更好的方式來分割這兩個功能:也許可以將 Twitter 字符串的解析移到一個不同的類中嗡官;或者創(chuàng)建一個不同的類來負責提供發(fā)帖排行榜箭窜。
開閉原則(OCP)
軟件實體(類,模塊衍腥,函數(shù)等等)應(yīng)該對擴展開放磺樱,但是對修改封閉。
作為審查者婆咸,如果發(fā)現(xiàn)通過一系列的 if
語句來檢查類型竹捉,你應(yīng)該意識到破壞了開閉原則。
如果你在審查上面的代碼尚骄,你應(yīng)該很清楚的意識到块差,如果一種新的 Event
類型添加到系統(tǒng)中,那么新的類型創(chuàng)建者為了處理新添加的類型倔丈,它也許必須添加另一個 else
語句到這個方法中憨闰。
使用多態(tài)來替換這些 if
可能會好一些:
和往常一樣,這個問題不止一個解決方法需五,但關(guān)鍵是將復(fù)雜的 if/else
和 instanceof
檢查去掉鹉动。
里氏替換原則(LSP)
使用了基類引用的函數(shù),在不知道基類子類的情況下宏邮,也能夠使用子類的對象
發(fā)現(xiàn)破壞這一規(guī)則的簡單方法就是關(guān)注顯式的類型轉(zhuǎn)換训裆。如果你必須將一個對象轉(zhuǎn)換為其他類型,那么你并沒有“在不知道子類信息的情況下”使用基類蜀铲。
在檢查 LSP 的以下兩個條件時,會發(fā)現(xiàn)更多微妙的破壞 LSP:
- (當子類的方法重載父類的方法時)方法的前置條件(即方法的形參)要比父類方法的輸入?yún)?shù)更寬松属百。
- (當子類的方法實現(xiàn)父類的抽象方法時)方法的后置條件(即方法的返回值)要比父類更嚴格记劝。
想象一下,例如我們有一個抽象類 Order
族扰,它有一系列子類 - BookOrder
厌丑,ElectronicsOrder
等等。Order
類的
PlaceOrder
方法接收 Warehouse
參數(shù)渔呵,并以此修改倉庫中的庫存水平:
現(xiàn)在假設(shè)我們引入了新的電子禮品卡怒竿,這個只需要往錢包里添加余額就可以,不需要實際的庫存扩氢。如果用 GiftCardOrder
類來實現(xiàn)電子禮品卡耕驰,placeOrder
方法就不必使用 warehouse
參數(shù):
這看起來像是合理的使用繼承,但事實上你是希望使用 GiftCardOrder
類的代碼能夠像使用其他類那樣使用它录豺,即你希望所有的子類都能通過測試:
但是這個測試并通不過朦肘,因為 GiftCardOrder
有不同的訂購行為饭弓。如果你在審查這一類代碼,確認這里使用繼承是否合理--也許訂購行為可以通過組合而不是繼承來插入媒抠。
接口分離原則(ISP)
多個明確的客戶端接口要好于一個通用的接口
如果代碼中有接口定義了很多個方法弟断,那么很容易確認它破壞了這一規(guī)則。這一條規(guī)則和 SRP 是一致的趴生,你可能會發(fā)現(xiàn)擁有多個方法的接口實際上會負責多個方面或者功能阀趴。
但是有時只有兩個方法的接口也應(yīng)該分為兩個接口:
在這個例子中,假設(shè)有時候不需要 decode
方法苍匆,并且某一個 codec 在不同的場合有可能當做 endoder 使用刘急,有時可能當做 decoder 使用,那么把 SimpleCodec
拆分成 Encoder
和 Decoder
更合適一些锉桑。有的類可能會同時實現(xiàn)這兩個接口排霉,但是不必讓所有的實現(xiàn)者都去 Override 它們不需要的方法,或者說只需要 Encoder
接口的類注意到它們的 Encoder
實例還實現(xiàn)了 decode
民轴。
依賴反轉(zhuǎn)原則(DIP)
依賴于抽象攻柠,而不是具體的實現(xiàn)。
發(fā)現(xiàn)簡單的破壞這一規(guī)則可能比較容易后裸,比如使用 new
關(guān)鍵字(而不是使用依賴注入或者工廠模式)或者對你的集合類型過度熟悉(例如將變量和參數(shù)定義為 ArrayList
而不是 List
)瑰钮,作為審查者,你應(yīng)該注意保證代碼作者使用/創(chuàng)建了正確的抽象微驶。
例如浪谴,服務(wù)級別的代碼使用直接和數(shù)據(jù)庫之間的連接來讀寫數(shù)據(jù):
這段代碼依賴于許多具體的實現(xiàn)細節(jié):數(shù)據(jù)庫連接 JDBC,數(shù)據(jù)庫特定的 SQL因苹,數(shù)據(jù)庫的結(jié)構(gòu)等等苟耻。這些代碼應(yīng)該出現(xiàn)在系統(tǒng)的某一個地方,但是不應(yīng)該出現(xiàn)在這里扶檐,也不應(yīng)該出現(xiàn)在不需要了解數(shù)據(jù)庫細節(jié)的方法中凶杖。更好的方法是提取出一個 DAO 或者使用 Repository 模式,然后將 DAO 或者 repository 注入到這個 services款筑。
總結(jié)
這些代碼“味道”可能表示一個或者多個 SOLID 原則被破壞:
- 很長的
if/else
語句 - 強制轉(zhuǎn)換到子類型
- 很多公共方法
- 實現(xiàn)了拋出
UnsupportedOperationException
的方法
與所有設(shè)計問題一樣智蝠,在遵循這些原則之間找到平衡,并根據(jù)你的團隊的喜好做出調(diào)整奈梳。 但是杈湾,如果在代碼審查中看到復(fù)雜的代碼,你可能會發(fā)現(xiàn)應(yīng)用這些原則之一會找到一個更簡單攘须,更易于理解的解決方案漆撞。