此篇文章偏重於以圖解方式,簡單帶大家了解 介面隔離原則 哦,有興趣就往下看吧!
本篇目錄
介面隔離原則 ISP
ISP 為 Interface Segregation Principle 簡寫,均意為介面隔離原則。
👍 若開發途中有持續遵循 ISP 原則的話,其實你會發現程式也漸漸會降低耦合哦!
換句話說,就是軟體模組之間依賴性沒有那麼強,解耦好處可參考之前寫的 DIP 文章。
定義
以下擷取自 wikipedia
No client should be forced to depend on methods it does not use.
不應強迫客戶端去依賴它不使用的方法!
見解
一般來說,都會盡量避免掉具體類別之間的依賴關係,因為風險實在太多了…
⭐️ 介面就是一個解除耦合的關鍵,如果操作介面得宜,就能改變依賴關係,進而改善系統架構。
❓ 那有什麼時機點可以很直覺套用 ISP 原則?回頭再來告訴各位。
類別圖探討
這邊依舊拿 SRP 最後討論出來的方案 C 做說明,如下原圖:
進一步再畫成如下圖(若有其它元件互動情況下):
有發現哪裡不對勁了嗎 Σ(*゚д゚ノ)ノ ?
回頭看完定義,再回來看匯出元件(Export Component),確實已經違反了 ISP 原則,為什麼?
假設今天有其它元件(Other Component)也想要利用匯出元件來匯出檔案,所以理論上工程師修改範圍可能就落在其它(Other)及匯出(Export)元件,但工程師卻忘了匯出元件有被「別的元件(Utility Component)」所使用,變動過大的話,這個所謂「別的元件」就需要重新編譯及部署!
再者,如果我們站在其它元件(Other Component)的角度來看,我可以給文章(Article)或檔案(file)來匯出,但這個其它元件真的會需要知道「給文章來匯出」這件事嗎?
Ans:如果說這個其它元件職責就只有「給檔案來匯出」,那它就不應該知道有「給文章來匯出」這件事!
. . .
如何讓其它元件避免知道它不應該需要知道的事?介面(Interface)此時就能派上用場了!
加上介面來隔離之後,如下圖:
紅圈處就是關鍵,這兩個介面(ExportFile、ExportArticle)達成了兩個目的!
- 資訊隱藏(可參考之前寫的 OCP 文章)
- 避免讓使用匯出元件者知道它不應該需要知道的事
使用 ISP 時機點
⭐️ 最直覺的時機點:使用他人所撰寫之第三方元件
❓ 你可能會覺得直接用第三方元件有什麼不對?幹嘛多一個介面來搞自己?
舉例
假設今天客戶有自己的 LDAP 元件(對主系統來說屬於外部元件),在進入主系統前需要先透過 LDAP 驗證是合法的使用者才可登入,程式碼(以 C# 為例子)你可能會這樣寫:
// AuthenticationMethod.cs
using xxx.ldap;
public class AuthenticationMethod {
// 驗證使用者密碼
public bool VerifyUserPassword(string userId, string password) {
// 預設驗證失敗
bool flag = false;
Ldap ldap = new Ldap();
try {
ldap.Open(...);
flag = ldap.VerifyUserPassword(userId, password);
} catch(Exception ex) {
// 錯誤處理...
} finally {
ldap.Close();
}
return flag;
}
}
VerifyUserPassword 如果功能要正常運行,勢必需要它人家的 LDAP 元件才行(間接來看就是AuthenticationMethod 和 Ldap 有耦合關係)。
❓ 當今天你要對 VerifyUserPassword 撰寫單元測試時,你會發現寫不出來 (((゚Д゚;))) ,為什麼?
回想一下它要怎麼樣能夠正常運行就知道原因了,因為 LDAP 沒辦法實際取得所需資料,沒取得資料也無從驗證是否斷言(Assert)會過…
再想一下,我只是要做單元測試而已,又不是真的要實際取得客戶的 LDAP 資料,所以我應該要想辦法偽造(Fake)一個 LDAP 元件出來,這樣就能順利驗證 VerifyUserPassword (無法取得 LDAP 連線情況下)。
. . .
那要怎麼偽造出一個 LDAP 元件?介面(Interface)這時又能派上用場了!
程式碼:
// ILdapBasic.cs
public interface ILdapBasic {
// 開啟連線
void Open(...);
// 驗證使用者密碼
bool VerifyUserPassword(string userId, string password);
// 關閉連線
void Close();
}
// LdapBasic.cs
using xxx.ldap;
public class LdapBasic : ILdapBasic {
private Ldap _Ldap;
public Ldap Ldap
{
get {
if (_Ldap == null) {
_Ldap = new Ldap();
}
return _Ldap;
}
set {
_Ldap = value;
}
}
// 開啟連線
public void Open(...) {
Ldap.Open(...);
}
// 驗證使用者密碼
public bool VerifyUserPassword(string userId, string password) {
return Ldap.VerifyUserPassword(userId, password);
}
// 關閉連線
public void Close() {
Ldap.Close();
}
}
// AuthenticationMethod.cs
public class AuthenticationMethod {
private ILdapBasic _LdapBasic;
public ILdapBasic LdapBasic
{
get {
if (_LdapBasic == null) {
_LdapBasic = (ILdapBasic)Assembly.Load(...).CreateInstance(...);
}
return _LdapBasic;
}
set {
_LdapBasic = value;
}
}
// 驗證使用者密碼
public bool VerifyUserPassword(string userId, string password) {
// 預設驗證失敗
bool flag = false;
try {
LdapBasic.Open(...);
flag = LdapBasic.VerifyUserPassword(userId, password);
} catch(Exception ex) {
// 錯誤處理...
} finally {
LdapBasic.Close();
}
return flag;
}
}
從上面程式碼可以觀察到幾個有趣的現象:
- AuthenticationMethod 本來有 xxx.ldap 的引用(include)被拿掉了,已經轉嫁給 LdapBasic 去承擔了(所以 LdapBasic 裡面的實作都會跟 xxx.ldap 息息相關)。
- AuthenticationMethod 現在是依賴 ILdapBasic 介面,而不是 xxx.ldap 中的具體實作(解耦)。
- 現在可以透過偽造 ILdapBasic ,進而達到可以於單元測試中給出預期的回應,這樣一來 VerifyUserPassword 即使不用真的連上 LDAP,就能夠進行模擬回應。
總結
使用第三方元件之前,先要評估外部帶給系統的影響,如果說要將系統跟外部做隔離,那就是會建議墊一層介面,介面本身就帶有侷限特性,而實作該介面的具體實作內部應該就都會是跟外部有關的實作!
迷思
❓ 好像寫單元測試會需要一併異動”被測試”程式?
Ans:如果有發生這種現象,那很有可能你不是在寫單元測試,反而可能是在做整合測試…
從剛剛套用 ISP 原則的程式碼來看,LdapBasic 屬性只有在功能正常運行時,才會取得真正的實例(_LdapBasic),而如果在單元測試執行途中,你需要做的是偽造一個實例出來並把它設定給 LdapBasic 屬性,這樣一來就不存在你寫單元測試會一併調整被測試程式這件事情 (´・ω・`)…
結尾
感謝各位花時間看完此篇文章,如果本文中有描述錯誤,還請各位指教。
希望這篇文章可以讓大家了解 Interface Segregation Principle,一般來說我們都會希望自己的系統好維護,其中透過使程式不去依賴它所不需要的東西,看似一件單純的事情,而真正有意識到的卻是少數,介面本身除了有規範的效果,但同樣也有制約的效果,如果介面操作得宜,將能使系統逐步單純化!