使用 ThreadStatic 而不是 DI 容器有什麼問題
我正在嘗試使一個非常大、非常遺留的項目可測試。
我們的大多數程式碼都使用了許多靜態可用的服務。問題是這些很難模擬。他們曾經是單身人士。現在它們是偽單例——相同的靜態介面,但函式委託給可以切換的實例對象。像這樣:
class ServiceEveryoneNeeds { public static IImplementation _implementation = new RealImplementation(); public IEnumerable<FooBar> GetAllTheThings() { return _implementation.GetAllTheThings(); } }現在在我的單元測試中:
void MyTest() { ServiceEveryoneNeeds._implementation = new MockImplementation(); }到目前為止,一切都很好。在產品中,我們只需要一個實現。但是測試並行執行並且可能需要不同的模擬,所以我這樣做了:
class Dependencies { //set this in prod to the real impl public static IImplementation _realImplementation; //unit tests set these [ThreadStatic] public static IImplementation _mock; public static IImplementation TheImplementation { get {return _realImplementation ?? _mock; } } public static void Cleanup() { _mock = null; } }接著:
class ServiceEveryoneNeeds { static IImplementation GetImpl() { return Dependencies.TheImplementation; } public static IEnumerable<FooBar> GetAllTheThings() {return GetImpl().GetAllTheThings(); } } //and void MyTest() { Dependencies._mock = new BestMockEver(); //test Dependencies.Cleanup(); }我們選擇這條路線是因為建構子將這些服務注入到每個需要它們的類中是一個龐大的項目。同時,這些是我們程式碼庫中大多數功能所依賴的通用服務。
我知道這種模式是不好的,因為它隱藏了依賴關係,而不是使依賴關係顯式的建構子注入。
但是好處是:
我們可以立即開始單元測試,而不是進行 3 個月的重構然後進行單元測試。
我們仍然有全域變數,但這似乎比我們原來的要好。
雖然我們的依賴關係仍然是隱含的,但我認為這種方法比我們擁有的方法要好得多。除了隱藏的依賴關係之外,這是否比使用適當的 DI 容器更糟糕?我會遇到什麼問題?
它是一個糟糕的服務定位器。但你已經知道了。如果您的程式碼庫如此龐大,為什麼不開始部分遷移呢?向容器註冊單例實例,並在您接觸程式碼中的類時啟動建構子注入它們。然後,您可以將大部分零件留在(希望)工作狀態,並在其他任何地方獲得 DI 的好處。
理想情況下,沒有 DI 的零件會隨著時間的推移而收縮。您可以立即開始測試。
這稱為環境上下文。如果正確使用和實施,使用環境上下文沒有任何問題。可以使用環境上下文時有一些先決條件:
- 它必須是一個橫切關注點,返回一些價值
- 您需要一個本地預設值
- 你必須確保
null不能被分配。(改用Null 實現)對於不返回值的橫切關注點,例如日誌記錄,您應該更喜歡攔截。對於其他不是橫切關注點的依賴項,您應該進行建構子注入。
但是,您的實現有幾個問題(不會阻止分配 null、命名、無預設值)。以下是如何實現它:
public class SomeCrossCuttingConcern { private static ISomeCrossCuttingConcern default = new DefaultSomeCrossCuttingConcern(); [ThreadStatic] private static ISomeCrossCuttingConcern current; public static ISomeCrossCuttingConcern Default { get { return default; } set { if (value == null) throw new ArgumentNullException(); default = value; } } public static ISomeCrossCuttingConcern Current { get { if (current == null) current = default; return current; } set { if (value == null) throw new ArgumentNullException(); current = value; } } public static void ResetToDefault() { current = null; } }環境上下文具有這樣的優勢,即您不會因橫切關注點而污染您的 API。
但另一方面,關於測試,您的測試可能會變得依賴。例如,如果你忘記為一個測試設置你的模擬,如果模擬是由另一個測試設置的,它會正確執行。但是當它獨立執行或以不同的順序執行時,它將失敗。它使測試更加困難。