[英]Dependency injection with interfaces or classes
在使用依賴注入時,我一直認為我的接口和具體類之間有一對一的關系。 當我需要向接口添加方法時,我最終會破壞實現該接口的所有類。
這是一個簡單的例子,但我們假設我需要將一個ILogger
注入我的一個類中。
public interface ILogger
{
void Info(string message);
}
public class Logger : ILogger
{
public void Info(string message) { }
}
像這樣的一對一關系感覺就像代碼味道。 由於我只有一個實現,如果我創建一個類並將Info
方法標記為虛擬以在我的測試中覆蓋而不是僅為單個類創建一個接口,是否有任何潛在的問題?
public class Logger
{
public virtual void Info(string message)
{
// Log to file
}
}
如果我需要另一個實現,我可以覆蓋Info
方法:
public class SqlLogger : Logger
{
public override void Info(string message)
{
// Log to SQL
}
}
如果這些類中的每一個都具有可以創建漏洞抽象的特定屬性或方法,我可以提取出一個基類:
public class Logger
{
public virtual void Info(string message)
{
throw new NotImplementedException();
}
}
public class SqlLogger : Logger
{
public override void Info(string message) { }
}
public class FileLogger : Logger
{
public override void Info(string message) { }
}
我沒有將基類標記為抽象的原因是因為如果我想添加另一個方法,我不會破壞現有的實現。 例如,如果我的FileLogger
需要一個Debug
方法,我可以在不破壞現有SqlLogger
情況下更新基類Logger
。
public class Logger
{
public virtual void Info(string message)
{
throw new NotImplementedException();
}
public virtual void Debug(string message)
{
throw new NotImplementedException();
}
}
public class SqlLogger : Logger
{
public override void Info(string message) { }
}
public class FileLogger : Logger
{
public override void Info(string message) { }
public override void Debug(string message) { }
}
再一次,這是一個簡單的例子,但是當我應該更喜歡一個界面時?
“快速”答案
我會堅持使用接口。 它們旨在成為外部實體的消費合同。
@JakubKonecki提到了多重繼承。 我認為這是堅持使用界面的最大理由,因為如果你強迫他們選擇一個基類,它將在消費者方面變得非常明顯......沒有人喜歡基類被強加給他們。
更新的“快速”答案
您已經說明了控制之外的接口實現問題。 一個好的方法是簡單地創建一個繼承舊的接口並修復自己的實現。 然后,您可以通知其他團隊新的界面可用。 隨着時間的推移,您可以棄用舊接口。
不要忘記您可以使用顯式接口實現的支持來幫助在邏輯上相同但不同版本的接口之間保持良好的划分。
如果您希望所有這些都適合DI,那么盡量不要定義新的接口,而是支持添加。 或者,為了限制客戶端代碼更改,嘗試從舊的接口繼承新接口。
實施與消費
實現界面和使用它之間存在差異。 添加方法會破壞實現,但不會破壞使用者。
刪除方法顯然會破壞消費者,但不會破壞實現 - 但如果您對消費者具有向后兼容性,則不會這樣做。
我的經歷
我們經常與接口建立一對一的關系。 它在很大程度上是一種形式,但你偶爾會得到很好的實例,其中接口是有用的,因為我們存根/模擬測試實現,或者我們實際上提供客戶端特定的實現。 如果我們碰巧改變界面,這經常打破一個實現的事實不是代碼氣味,在我看來,它只是你如何對抗接口。
我們現在采用基於接口的方法,因為我們利用工廠模式和DI元素等技術來改善老化的遺留代碼庫。 在找到“確定的”用法之前,測試能夠快速利用代碼庫中存在接口的事實多年(即,不僅僅是具體類的1-1映射)。
基類缺點
基類用於向普通實體共享實現細節,在我看來,他們能夠通過公開共享API做類似的事情是副產品。 接口旨在公開共享API,因此請使用它們。
對於基類,您還可能會泄漏實現細節,例如,如果您需要為實現的另一部分公開使用某些內容。 這些都不利於維護一個干凈的公共API。
打破/支持實施
如果你沿着界面路線走下去,由於違約,你甚至可能難以改變界面。 此外,正如您所提到的,您可能會破壞控件之外的實現。 有兩種方法可以解決這個問題:
我目睹了后者,我看到它有兩種形式:
MyInterfaceV1
, MyInterfaceV2
。 MyInterfaceV2 : MyInterfaceV1
。 我個人不會選擇沿着這條路走下去,我會選擇不支持破壞變更的實現。 但有時我們沒有這個選擇。
一些代碼
public interface IGetNames
{
List<string> GetNames();
}
// One option is to redefine the entire interface and use
// explicit interface implementations in your concrete classes.
public interface IGetMoreNames
{
List<string> GetNames();
List<string> GetMoreNames();
}
// Another option is to inherit.
public interface IGetMoreNames : IGetNames
{
List<string> GetMoreNames();
}
// A final option is to only define new stuff.
public interface IGetMoreNames
{
List<string> GetMoreNames();
}
當您開始添加除Info
之外的Debug
, Error
和Critical
方法時, ILogger
接口正在破壞接口隔離原則 。 看看可怕的Log4Net ILog界面 ,你就會知道我在說什么。
不是按日志嚴重性創建方法,而是創建一個采用日志對象的方法:
void Log(LogEntry entry);
這完全解決了您的所有問題,因為:
LogEntry
將是一個簡單的DTO,您可以向其添加新屬性,而不會破壞任何客戶端。 Log
方法的ILogger
接口創建一組擴展方法。 以下是此類擴展方法的示例:
public static class LoggerExtensions
{
public static void Debug(this ILogger logger, string message)
{
logger.Log(new LogEntry(message)
{
Severity = LoggingSeverity.Debug,
});
}
public static void Info(this ILogger logger, string message)
{
logger.Log(new LogEntry(message)
{
Severity = LoggingSeverity.Information,
});
}
}
有關此設計的更詳細討論,請閱讀此內容 。
你應該總是喜歡這個界面。
是的,在某些情況下,您將在類和接口上使用相同的方法,但在更復雜的情況下,您不會。 還要記住,.NET中沒有多重繼承。
您應該將接口保存在單獨的程序集中,並且您的類應該是內部的。
對接口進行編碼的另一個好處是能夠在單元測試中輕松模擬它們。
我喜歡接口。 鑒於存根和模擬也是實現(有點),我總是至少有兩個任何接口的實現。 此外,可以對接口進行存根和模擬以進行測試。
此外,Adam Houldsworth提到的合同角度是非常有建設性的。 恕我直言,它使代碼更清潔,而不是1-1的接口實現讓它變得臭。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.