设计模式:面向对象的设计原则下(ISP、DIP、KISS、YAGNI、DRY、LOD)
本文繼續來介紹接口隔離原則(ISP)和依賴倒置原則(DIP),這兩個原則都和接口和繼承有關。文章最后會簡單介紹幾個除了 SOLID 原則之外的原則。
接口隔離原則(ISP)
提起接口,開發人員的第一反應可能是面向對象編程語言中的 interface ,但接口更廣義的理解會包含:
編程語言中的 interface;
RESTful Web API 、Web Service、gRPC 等這種對外提供服務的接口;
類庫中的公共方法。
不管是上面的哪一種,要想設計好,就需要用到接口隔離原則了。
接口隔離原則的定義是:
不應強迫使用者依賴于它們不用的方法。
接口被設計出來后,就會有地方對接口進行調用,調用的地方希望接口中提供的方法都是他需要的,所以在接口設計的時候,需要考慮應該將哪些方法放入其中,讓調用者使用,這就是對定義的解釋。
相反,如果不精心設計,接口就會變得越來越龐大,會帶來兩個問題:
1、在一個更高層的接口中添加一個方法只是為了某一個子類使用,所有的子類都必須對其實現,或提供一個默認實現;
2、接口中包羅萬象,調用者可能會誤用其中的方法。
舉個例子:我們現在正在開發 SaaS 產品,里面會涉及到對租戶的操作,比如租戶需要注冊、登錄等,抽象成接口代碼如下:
public?interface?ITenant {public?void?Register(string?mobile,string?password);public?void?Login(string?mobile,string?password); } public?class?Tenant?:?ITenant {public?void?Register(string?mobile,?string?password){throw?new?NotImplementedException();}public?void?Login(string?mobile,?string?password){throw?new?NotImplementedException();} }上面的操作是針對租戶這個角色的,現在有新的需求來了,對于 SaaS 廠商的管理員來說,希望能禁用租戶,一種偷懶的做法就是直接在 ITenant 接口中添加禁用的方法,如下:
public?interface?ITenant {public?void?Register(string?mobile,string?password);public?void?Login(string?mobile,string?password);public?void?Diabled(string?tenantCode); } public?class?Tenant?:?ITenant {//?...public?void?Diabled(string?tenantCode){throw?new?NotImplementedException();} }上面的代碼就違反了接口隔離原則,因為在普通租戶的使用場景下,并不希望能調用到 Diabled 方法,正確的做法是將這個方法抽象到一個新的接口中,如下:
public?interface?ITenant {public?void?Register(string?mobile,string?password);public?void?Login(string?mobile,string?password);} public?interface?ITenantForAdmin {public?void?Diabled(string?tenantCode); }可以看出來,改造之后,每個接口的職責更加單一了,好像跟單一職責有點類似,仔細想想,還是有些區別,單一職責原則針對的是方法、類和接口的設計。而接口隔離原則更側重于接口的設計,另一方面就是思考的角度不同,在上面例子中,按照普通租戶和管理員兩種不同角色的維度來思考并進行拆分。
依賴倒置原則(DIP)
這個原則的名字中有兩個關鍵詞「依賴」和「倒置」,先來看看這兩個詞是什么意思?
依賴:在面向對象的語言中,所說的依賴通常指類與類之間的關系,比如有個用戶類 User 和日志類 Log , 在 User 類中需要記錄日志,就需要引入日志類 Log,這樣 User 類就對 Log 類產生了依賴,代碼如下:
public?class?User {private?Log?_log=new?Log();public?string?GetUserName(){_log.Write("獲取用戶名稱");return?"oec2003";} } public?class?Log {public?void?Write(string?message){Console.WriteLine(message);} }倒置:有依賴的倒置,那肯定就有正常的依賴,我們正常的編程思維都是從上而下來編寫業務邏輯的,遇到分支就寫 if ,遇到循環就寫 for ,需要創建對象就 new 一個,就像上面的代碼,上面的代碼就是一種正常的依賴。User 類依賴了 Log 類,如果倒置了,那就是 User 類不再依賴 Log 類了,下面會進一步來解釋。
正常的依賴會帶來的問題是:User 類和 Log 類高度耦合,當有一天我們想使用 NLog 或者 Serilog 替換 Log 類時,就需要改動 User 類,說明日志類的實現是不穩定的,而依賴一個不穩定的東西,從架構設計的角度來看,不是一個好的做法。解決此問題就需要用到依賴倒置原則。
先來看看依賴倒置原則的定義:
高層模塊不應依賴于低層模塊,二者應依賴于抽象。
抽象不應依賴于細節,細節應依賴于抽象。
什么是高層模塊?什么是低層模塊?按照上面的代碼示例,User 類是高層模塊,Log 類是低層模塊,二者都要依賴于抽象,就需要提取接口了:
public?interface?ILog {public?void?Write(string?message); } public?class?Log:ILog {public?void?Write(string?message){Console.WriteLine(message);} } public?class?User {private?ILog?_log;public?User(ILog?log){_log?=?log;}public?string?GetUserName(){_log.Write("獲取用戶名稱");return?"oec2003";} }調整后的代碼 User 類中依賴變成了 ILog 接口,日志的實現類 Log 也依賴 ILog 接口,即從 ILog 接口繼承而來,現在都是依賴 ILog 接口,這就是依賴倒置。
當想要將日志組件替換為 NLog 時,只需要創建一個新的類 NLogAdapter 類繼承 ILog 接口,在 NLogAdapter 類中引入 NLog 組件。
public?class?NLogAdapter:ILog {private?NLog?_log=new?NLog();public?void?Write(string?message){_log.Write(message);} }這樣,當日志組件替換的時候,User 類就不用修改了,因為 User 類的構造函數中使用的是 ILog 接口來接收的日志組件的對象,那到底是誰決定傳遞 Log 對象還是 NLogAdapter 對象呢?這就要引入一個新的概念叫「依賴注入」。
關于依賴注入可以看我之前寫的兩篇文章:
dotNET Core 3.X 依賴注入
dotNET Core 3.X 使用 Autofac 來增強依賴注入
依賴倒置是一種架構設計思想,指導架構層面的設計,依賴注入則是一種具體的編碼技巧,用來實現這種設計思想。
其他原則
除了 SOLID 五大原則之外,還有一些原則也在指引我們設計好的代碼架構方面發揮著作用:
KISS
YAGNI
DRY
LOD
KISS
KISS 的全稱是:Simple and Stupid ,該原則就是告訴我們,在設計時要盡量保持簡單,大道至簡嘛。這里的簡單不完全是指代碼的簡潔。現在已經不是單打獨斗的時代,大部分情況下開發人員都是在一個團隊中協同工作,所以我認為對簡單的理解可以分為:
代碼的可讀性要強,團隊要遵循一定的規范;
不要使用一些你認為很“高深”的技巧,應該使用團隊都熟知或者較為廣泛的編碼方式;
避免過度設計,一個很簡單的邏輯或者一些一次性的業務為了秀技術而設計的非常復雜是大可不必的。
將復雜的東西能夠深入淺出,做到簡單、簡潔,這是能力的體現。
YAGNI
YAGNI 的全稱是:You Ain’t Gonna Need It。直譯就是:你不會需要它。核心思想就是指導我們不要做過度設計。
1、當我們能識別到代碼的變化點的時候,可以預留擴展點,但不要提前做復雜的實現;
2、持續重構來優化代碼,而不是一開始就提取各種通用方法,例如一個私有函數只有一個調用的時候,就放在類里面,離調用者最近的地方,當有不止一處都會使用時,再考慮重構來進行通用方法的抽取。
過度設計會浪費資源,讓代碼復雜度變大,難以閱讀和維護。
DRY
DRY 的全稱是:Don’t Repeat Yourself ,就是不要重復自己,提升代碼的復用性,告別 CV 大法。
很多初級程序員都喜歡面向 Ctrl+C、Ctrl+V 編程,當需求變化的時候,很容易就遺漏一些場景,但即便是復制粘貼也不完全都是違反 DRY 。
代碼的重復有兩種情況:
1、代碼的邏輯重復,語義也重復:這種違反了 DRY ,需要進行重構;
2、代碼的邏輯重復,語義不重復:在某個階段,兩段代碼邏輯是相同的,但其實是兩種不同的應用場景,語義不一樣,就沒有違反 DRY。如果對這種代碼進行重構提取成公共方法,隨著業務發展,兩種不同的場景獨立演化了,稍不注意,代碼中就會出現各種 if 判斷,影響可讀性和可維護性。
LOD
LOD 全稱是:The Least Knowledge Principle ,也被稱之為迪米特法則。該法則有兩條指導原則:
1、不該有直接依賴關系的類之間,不要有依賴;
2、有依賴關系的類之間,盡量只依賴必要的接口。
其實就是一直流傳的代碼要高內聚、低耦合,單一職責和接口隔離想要表達的也是這個意思,區別只是側重點有所不同:
單一職責:針對的是方法、類和接口的設計,關注的是方法、類本身;
接口隔離:針對的是接口拆分、關注的是調用者的角色;
迪米特:關注類之間的關系。
各種原則之間相輔相成,有很多只是有些細微的差別,慢慢理解原理,才能以不變應萬變。
做項目和產品的軟件開發,為了使代碼能易讀、可擴展、可復用,我們需要遵循這些原則來進行架構設計,做平臺級的產品更是如此,比如我們的低代碼平臺,如有興趣歡迎掃下面二維碼進群討論。
總結
以上是生活随笔為你收集整理的设计模式:面向对象的设计原则下(ISP、DIP、KISS、YAGNI、DRY、LOD)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET 大会今日开幕 |这些白嫖福利不
- 下一篇: C# 值得永久收藏的WPF项目实战(经典