設計模式:面向對象的設計原則下(ISP、DIP、KISS、YAGNI、DRY、LOD)

語言: CN / TW / HK

本文繼續來介紹接口隔離原則(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 對象呢?這就要引入一個新的概念叫「依賴注入」。

關於依賴注入可以看我之前寫的兩篇文章:

依賴倒置是一種架構設計思想,指導架構層面的設計,依賴注入則是一種具體的編碼技巧,用來實現這種設計思想。

其他原則

除了 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、有依賴關係的類之間,儘量只依賴必要的接口。

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

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

  • 接口隔離:針對的是接口拆分、關注的是調用者的角色;

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

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

做項目和產品的軟件開發,為了使代碼能易讀、可擴展、可複用,我們需要遵循這些原則來進行架構設計,做平台級的產品更是如此,比如我們的低代碼平台,如有興趣歡迎掃下面二維碼進羣討論。