您好,登錄后才能下訂單哦!
最近在讀《架構整潔之道》這一本書,這本書的確寫得不錯,最近也沒有更新文章,一方面再忙工作,另一方面也再啃一些書。當然文章還是得更新,《架構整潔之道》里面有些有意思的內容我會提取出來外加自己的思考。在這本書里面的第三章介紹了設計原則,這部分我覺得對于大家的平時工作都比較有用。
想必大家在學習面向對象的時候,都學習過下面幾大原則:
SRP 單一職責:該設計原則是基于康威定律的推論,每個軟件模塊有且只有一個被更改的理由。
OCP 開閉原則:對擴展開放,對修改關閉。
LSP 里氏替換原則:任何基類可以出現的地方,子類一定可以出現。
ISP 接口隔離原則:在設計中需要避免不需要的依賴。
DIP 依賴反轉原則:高層策略性代碼不應該依賴底層細節的代碼,而應該是底層細節代碼依賴高層策略。
這五個原則也被稱為,SOLID原則取的是他們的首字母。這個也是我們做一個好設計的基礎,接下來會依次對其進行解釋。
SRP很容易被大家從字面意思無界,并不是每個模塊只做一個事,而是每個模塊的變化原因只有一個。在書中對于SRP最后的解釋是:
任何一個軟件模塊都應該只對某一類行為者(有共同需求的人)負責。
這里的軟件模塊指的就是一個源代碼文件或者一組緊密相關的函數和數據結構。SRP原則應該是大家運用得最多的原則之一。在書中舉了一個例子,有一個Employee類其中有三個函數:
calculatePay():計算工資,由財務部門制定,需要向CFO匯報。
reportHours():計算工時,人力資源制定,向COO匯報
save():由DBA制定,向CTO匯報。
這里三個函數都放在了Employee類中,其實也就是把三個行為者的行為都耦合在了一起。一般來說計算工資,會獲取正常工時,而計算工時也會獲取工時,這兩個函數都依賴了一個獲取工時的方法,如果財務部門計算工資時,想修改邏輯,看大家辛苦了1個小時當1.1個小時發工資,這個時候修改了這個獲取工時的方法,但是HR部門并不需要這個修改,這個時候就會導致reportHours()這個方法出現數據錯誤。所以這個時候就需要將不同行為者的代碼就行拆分。
在書中給出了第一個解決方法:
設計出三個類,每個類都只與一個行為者相關。這種問題的壞處是,程序員需要在程序里處理三個類,這里還介紹了使用門面模式的方法,讓我們只需要在我們使用的地方使用一個類即可:
這樣的話我們就不需要關心其他三個類,直接調用門面模式的方法即可。
在實際場景中微服務可以算作是SRP的思想,雖然每一個微服務不止一個類,但是其整個服務也可以看做是一個模塊,而每個一個模塊基本也只于一個行為者相關。在我們的代碼中可以使用3.1中所描述的方法來進行SRP的實現。
SRP的好處:
修改代碼容易,由于不需要考慮修改代碼是否會影響其他業務所以是很容易的。
更加容易維護,維護一個什么邏輯都有的代碼明顯比維護一個單一職責的代碼難得多。
容易發現問題,當出現問題的時候,由于職責清晰,可以比較容易的定位。
松耦合,職責分離,耦合程度比較低。
在這本書中講述OCP可能和大家從一些資料上面看的有點不同。一般大家所認為的開閉原則,應該將那些容易變化的部分進行抽象,利用對抽象的多個實現來進行對擴展開放,而不是直接在類中去修改。
這里我用吃飯的例子來列舉:
每個人一天都會吃三餐,早餐,午餐,晚餐,但是隨著時代的進步,又出現了下午茶,宵夜等,現在一天就不止三餐,那么其實我們就需要在這個類中去添加喝下午茶方法,吃宵夜方法,這樣就導致我們沒增加一個餐的種類就需要添加一個方法,在將SRP的時候我們有個例子,在同一個類中修改方法的時候容易修改其他業務邏輯,在我們這個例子中我們也會出現這個問題。怎么解決呢?那么我們就可以將變化的部分抽象出來: ,后續如果還需要增加吃的方法那么只需要實現這個接口即可。
但是在這本書中,并沒有去強調將變化的部分抽象出來,其認為修改是不可避免的,所以我們需要把控好修改的影響,所以提出了高層組件的修改不會影響底層組件,組件層次越低越穩定。對于J2EE的開發者來說,三層開發肯定并不陌生,controller,service,dao:
如果我們修改controller那么service其實是無感知的,不會受影響,如果我們修改service,dao是不會受影響的,但是我們的controller是會受影響。所以越底層的組件那么其實應該越穩定。通過這種方式我們可以控制修改范圍的影響。總結起來就是通過將系統劃分為一系列組件,并且將這些組件間的依賴關系按層次結構進行組織,使得高階組件不會因低階組件被修改而受到影響。
任何基類可以出現的地方,子類一定可以出現。大多數人認為LSP其實就是指導如何使用繼承關系的一種方法,尤其是我們在開發的過程中用spring依賴注入的基本都是基類而非具體的實現類,這個的確也是LSP的一種實現手段。LSP也在逐漸演變成一種更廣泛的,指導接口與其實現方式的設計原則。
這里用書中的一個反面例子來舉例,假如我們現在在構建一個提供出租車調度服務的系統,我們提供restful進行調用,有這么一個司機,如果我們想調度他那么需要訪問以下請求:
purplecab.com/driver/Bob/pickupAddress/24 Maple St./pickupTime/153/destination/ORD
每個公司想要接入我們的系統都得遵循上面的規矩,但是有一個公司Acme把destination寫成了縮寫dest,但是由于這個公司比較大,不想修改回來,所以調度系統只能寫如下的if邏輯:
if(driver.getDispatchUri().startsWith("acme.com"))
這種邏輯一般的軟件架構師都不會允許這樣一條語句出現在系統中,如果又出現了一個公司違反了那么是否又需要增加一條if邏輯?軟件架構師應該創建一個調度請求創建組件,并讓該組件使用一個配置數據庫來保存URI組裝格式,如下:
URI | 調度格式 |
---|---|
Acme.com | /pickupAddress/%s/pickupTime/%s/dest/%s |
. * . | /pickupAddress/%s/pickupTime/%s/destination/%s |
但是這樣我們也需要增加一個組件來應對這個情況,也增加了復雜性。
首先大家看看下面這個例子:
我們這里的User1,User2,User3都是依賴OPS的,但是User1只需要用op1,User2用op2,User3用op3。在這種情況下,雖然User1不會和op2,op3產生直接的調用關系,但在源代碼層次上也與他們形成依賴關系。這種依賴關系會導致兩個問題:修改op2,op3的邏輯會導致op1的邏輯變化
就算邏輯不變化,修改op2也會導致重新編譯和部署User1。
我們通過下面這種方將不同的操作隔離成接口,運用第5節的LSP,我們將OPS類實現這三個接口,然后替換在User1中的U1Ops,由于依賴的是最小接口所以就不會出現上面的問題。
在書中對于ISP強調得比較多,在后面也講了CRP原則,不要強迫一個組件的用戶依賴他們不需要的東西,CRP是ISP的一個普適版。ISP是針對類來說,CRP是針對組件來說。所以我們總結起來就是:
不要依賴不需要的東西
依賴反轉其實總結起來就是多依賴抽象,少依賴具體實現。但是事事并沒有那么絕對,我們的String類是一個具體的實現類但是在我們的代碼中隨處可見,那是不是我們就違反了DIP了呢?其實不是的,我們String已經非常穩定了,就算修改也會被嚴格的控制,所以我們不需要擔心修改String類會發生一些意想不到的問題。所以對于我們穩定的東西,其實DIP原則就不適用了,而我們需要重點關注的應該經常變動的。這里我想要說的一點的是,大家在編碼過程中寫List的時候雖然大多數時候用的是ArrayList,但是其實很少寫下面這句話ArrayList list = new ArrayList(),更多的是寫List list = new ArrayList(),其實這個就是DIP的一個實現。
這里要說一下為什么叫反轉?有反轉就有正轉,如下圖所示:
這里的我們的serviceImpl是service的具體實現,在圖中我們的依賴流向沒有發生變化,所以叫正轉。我們采用DIP來進行設計:DIP還有幾個編碼規則需要注意:
多使用抽象接口,盡量避免使用具體實現類。
盡量不要在具體實現類上面創建子類。
盡量不要覆蓋繼承的抽象類的方法:由于我們依賴的是抽象,有可能邏輯中已經對這些方法產生了依賴,如果覆蓋有可能會造成問題。
本文講了一下設計的五大原則SOLID,SOLID在這《架構整潔之道》中一直貫穿,這五大原則能幫助我們在設計的時候做出更多優秀的架構設計,如果想了解更多的一些細節可以看看這本書。
如果你覺得這篇文章對你有文章,可以關注我的技術公眾號,也可以加入我的技術交流群進行更多的技術交流。你的關注和轉發是對我最大的支持,O(∩_∩)O。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。