在類庫里注入依賴

jopen 8年前發布 | 5K 次閱讀 開發

在類庫里注入依賴

在你的類庫中使用依賴注入和服務器定位的一個簡單的方式。

類庫中的控制反轉

框架的開發總是非常有趣的。下面是如何使用控制反轉原則為第三方開發者構建并發布類的一個快速提示。

背景

當你設計框架時,總是要為客戶端開發人員提取獨立的接口和類的。現在你有一個簡單數據訪問類 ModelService,是在一個叫做 SimpleORM 的框架中被發布的。

你已經做了職責分離和接口隔離,并(通過組合)使用了幾個其他的接口 IConnectionStringFactory,IDTOMapper,IValidationService 做了你的 ModelService 類的設計。

你想要將依賴的接口注入到你的 ModelService 類,這樣你可以對其進行適當的測試。這可以很容易的使用構造方法注入來實現:

public ModelService(IConnectionStringFactory factory, IDTOMapper mapper, IValidationService validationService)
{
    this.factory = factory;
    this.mapper = mapper;
    this.validationService = validationService;
}

當以你自己的應用的模塊為中心時這種類型的依賴注入是經常使用的,并且你不會將其發布為單獨的類庫。你不用擔心如何實例化你的 ModelService 類,因為你將為 ModelService 實例查詢你的 DI 容器。最后將會注入 IConnectionStringFactory,IDTOMapper,IValidationServiceor 或任何其他的結合。

另一方面,當你為第三方使用者發布你的類時,該方案略有不同。你不想讓調用者能夠注入他想要的任何接口到你的類中。此外,你不想讓他擔心他需要為構造方法傳遞任何接口的實現。除了 ModelService 類之外的一切都要被隱藏。

理想的情況下,他只要使用下面的語句就能夠獲得你的 ModelService 類的一個實例:

var modelService = new ModelService();

當你允許調用者改變你的類的行為時上述說法不成立。如果你正在實現策略模式或裝飾模式,你定義的構造函數將明顯的稍有不同。

最簡單的方式

實現可測試性并為框架調用者留下一個無參構造方法最簡單的方式如下:

public ModelService() : this(new ConnectionStringFactory(), new DTOMapper(), new ValidationService()
{
   // no op
} 

internal ModelService(IConnectionStringFactory factory, IDTOMapper mapper, IValidationService validationService)
{
    this.factory = factory;
    this.mapper = mapper;
    this.validationService = validationService;
}

假如你正在一個單獨的測試項目中測試你的 ModelService 類,別忘了在 SimpleORM 的配置文件中設置 InternalVisibleTo 參數:

[assembly: InternalsVisibleTo("SimpleORM.Test")]

上面描述的方式有雙重的優點:它將允許你在你的測試中注入 mock,以及為你框架的用戶隱藏帶參構造方法:

[TestInitialize]
public void SetUp()
{
      var factory = new Mock<IConnectionStringFactory>();
      var dtoMapper = new Mock<IDTOMapper>();
      var validationService = new Mock<ivalidationservice>();

      modelService = new ModelService(factory.Object, dtoMapper.Object, validationService.Object);
}

擺脫依賴

上述方法有個明顯的缺點:你的 ModelService 類有一個直接依賴復合類:ConnectionStringFactory,DTOMapper 和 ValidationService。 這違反了松耦合原則,會讓你得 ModelService 類靜態依賴于實現的服務之上。為了擺脫這些依賴,編程達人會建議你添加一個 ServiceLocator 來負責對象的實例化:

internal interface IServiceLocator
{
    T Get<T>();
}
 
internal class ServiceLocator
{
   private static IServiceLocator serviceLocator;
   
   static ServiceLocator()
   {
        serviceLocator = new DefaultServiceLocator();
   }

   public static IServiceLocator Current
   {
      get
      {
           return serviceLocator;
      }
   }

   private class DefaultServiceLocator : IServiceLocator
   {
      private readonly IKernel kernel;  // Ninject kernel
      
      public DefaultServiceLocator()
      {
          kernel = new StandardKernel();
      }

      public T Get<T>()
      {
           return kernel.Get<T>();
      }
   }
}

我寫了一個使用 Ninject 依賴注入框架的典型的 ServiceLocator 類。你可以使用任何你想要的 DI 框架,因為這對調用者來說是透明的。如果你關注性能,可以查看這個有趣的評估文章。另外,注意 ServiceLocator 類及其對應的接口是 internal 的。

 現在為依賴類調用 ServiceLocator 來取代直接實例化:

public ModelService() : this(
ServiceLocator.Current.Get<IConnectionStringFactory>(), 
ServiceLocator.Current.Get<IDTOMapper>(), 
ServiceLocator.Current.Get<IValidationService>())
{
   // no op
}

你要在你代碼的某處為IConnectionStringFactory,IDTOMapper 和 IValidationService 顯式的定義默認的綁定:

internal class ServiceLocator
{
   private static IServiceLocator serviceLocator;
   
   static ServiceLocator()
   {
        serviceLocator = new DefaultServiceLocator();
   }

   public static IServiceLocator Current
   {
      get
      {
           return serviceLocator;
      }
   }

   private sealed class DefaultServiceLocator : IServiceLocator
   {
      private readonly IKernel kernel;  // Ninject kernel
      
      public DefaultServiceLocator()
      {
          kernel = new StandardKernel();
          LoadBindings();
      }

      public T Get<T>()
      {
           return kernel.Get<T>();
      }
    
      private void LoadBindings()
      {
          kernel.Bind<IConnectionStringFactory>().To<ConnectionStringFactory>().InSingletonScope();
          kernel.Bind<IDTOMapper>().To<DTOMapper>().InSingletonScope();
          kernel.Bind<IValidationService>().To<ValidationService>().InSingletonScope();
      } 
   } 
}

跨類庫共享依賴

當你繼續開發你的 SimpleORM 框架,你最終會將你的類庫分離到不同的子模塊。比如你要為一個實現 NoSQL 數據庫交互的類提供一個擴展。你不想使用不必要的依賴搞亂你的 SimpleORM 框架,于是你單獨發布 SimpleORM.NoSQL 模塊。你要如何訪問 DI 容器?另外,你如何在你的 Ninject 內核中添加額外的綁定?

下面是一個簡單的解決方案。在你的初始類庫 SimpleORM 中定義一個接口 IModuleLoader:

public interface IModuleLoader
{
    void LoadAssemblyBindings(IKernel kernel);
}

不在你的 ServiceLocator 類中直接綁定接口到他們實際的實現,而是實現 IModuleLoader 并調用綁定:

internal class SimpleORMModuleLoader : IModuleLoader
{
   void LoadAssemblyBindings(IKernel kernel)
   {
      kernel.Bind<IConnectionStringFactory>().To<ConnectionStringFactory>().InSingletonScope();
      kernel.Bind<IDTOMapper>().To<DTOMapper>().InSingletonScope(); 
      kernel.Bind<IValidationService>().To<ValidationService>().InSingletonScope();
   }
}

現在你只需要從你得服務定位器類中調用 LoadAssemblyBindings。實例化這些類就成為了反射調用的問題:

internal class ServiceLocator
{
   private static IServiceLocator serviceLocator;

   static ServiceLocator()
   {
        serviceLocator = new DefaultServiceLocator();
   }

   public static IServiceLocator Current
   {
      get
      {
           return serviceLocator;
      }
   }

   private sealed class DefaultServiceLocator : IServiceLocator
   {
      private readonly IKernel kernel;  // Ninject kernel
      
      public DefaultServiceLocator()
      {
          kernel = new StandardKernel();
          LoadAllAssemblyBindings();
      }

      public T Get<T>()
      {
           return kernel.Get<T>();
      }
    
     private void LoadAllAssemblyBindings()
     {
         const string MainAssemblyName = "SimpleORM";
         var loadedAssemblies = AppDomain.CurrentDomain
                               .GetAssemblies()
                               .Where(assembly => assembly.FullName.Contains(MainAssemblyName));

        foreach (var loadedAssembly in loadedAssemblies)
        {
              var moduleLoaders = GetModuleLoaders(loadedAssembly);
              foreach (var moduleLoader in moduleLoaders)
              {
                  moduleLoader.LoadAssemblyBindings(kernel);
              }
         }
     }

     private IEnumerable<IModuleLoader> GetModuleLoaders(Assembly loadedAssembly)
     {
        var moduleLoaders = from type in loadedAssembly.GetTypes()
                                      where type.GetInterfaces().Contains(typeof(IModuleLoader))
                                      type.GetConstructor(Type.EmptyTypes) != null
                                      select Activator.CreateInstance(type) as IModuleLoader;
      return moduleLoaders;
     }
}

這段代碼作用如下:它在你的 AppDomain 中為 IModuleLoader 的實現查詢所有加載的部件。一旦發現,它將單例創建實例,確保為所有的模塊使用相同的容器。

你的擴展框架 SimpleORM.NoSQL 必須實現它自己的 IModuleLoader 類,以便它會被實例化并在第一次調用 ServiceLocator 類時被調用。顯然,以上代碼意味著你的 SimpleORM.NoSQL 依賴于 SimpleORM,擴展模塊依賴其父模塊是很正常的。

承諾

本文所描述的解決方案不是萬靈藥。它有它自己的缺點:管理可支配資源,偶爾在依賴模塊中重新綁定,創建實例時的性能開銷等。之后必須謹慎的使用一套良好的單元測試。如果你對上面的實現有任何意見非常歡迎你去評論區進行討論。

歷史

  • 2014 年 3 月 - 第一次發表

  • 2015 年 12 月 - 復查

許可

本文,及其任何相關的源碼和文件,遵循 The Code Project Open License (CPOL)

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!