設計模式:面向物件的設計原則下(ISP、DIP、KISS、DRY、LOD)

作者:oec2003

公眾號:不止dotNET

本文繼續來介紹介面隔離原則(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、有依賴關係的類之間,儘量只依賴必要的介面。

其實就是一直流傳的程式碼要高內聚、低耦合,單一職責和介面隔離想要表達的也是這個意思,區別只是側重點有所不同:

單一職責:針對的是方法、類和介面的設計,關注的是方法、類本身;

介面隔離:針對的是介面拆分、關注的是呼叫者的角色;

迪米特:關注類之間的關係。

各種原則之間相輔相成,有很多隻是有些細微的差別,慢慢理解原理,才能以不變應萬變。