2KB项目,专业的源码交易网站 帮助 收藏 每日签到

在类库里注入依赖

  • 时间:2019-01-23 18:29 编辑:2KB 来源:2KB.COM 阅读:351
  • 扫一扫,手机访问
  • 分享
摘要: 英文原文:Dep
英文原文:Dependency injection in class libraries

在你的类库中使用依赖注入和服务器定位的一个简单的方式。

类库中的控制反转

框架的开发总是非常有趣的。下面是如何使用控制反转原则为第三方开发者构建并发布类的一个快速提示。

背景

当你设计框架时,总是要为客户端开发人员提取独立的接口和类的。现在你有一个简单数据访问类 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)

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。 2KB翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。


2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务

  • 全部评论(0)
资讯详情页最新发布上方横幅
最新发布的资讯信息
【计算机/互联网|】Nginx出现502错误(2020-01-20 21:02)
【计算机/互联网|】网站运营全智能软手V0.1版发布(2020-01-20 12:16)
【计算机/互联网|】淘宝这是怎么了?(2020-01-19 19:15)
【行业动态|】谷歌关闭小米智能摄像头,因为窃听器显示了陌生人家中的照片(2020-01-15 09:42)
【行业动态|】据报道谷歌新闻终止了数字杂志,退还主动订阅(2020-01-15 09:39)
【行业动态|】康佳将OLED电视带到美国与LG和索尼竞争(2020-01-15 09:38)
【行业动态|】2020年最佳AV接收机(2020-01-15 09:35)
【行业动态|】2020年最佳流媒体设备:Roku,Apple TV,Firebar,Chromecast等(2020-01-15 09:31)
【行业动态|】CES 2020预览:更多的流媒体服务和订阅即将到来(2020-01-08 21:41)
【行业动态|】从埃隆·马斯克到杰夫·贝佐斯,这30位人物定义了2010年代(2020-01-01 15:14)
联系我们

Q Q: 7090832

电话:400-0011-990

邮箱:7090832@qq.com

时间:9:00-23:00

联系客服
商家入住 服务咨询 投拆建议 联系客服
0577-67068160
手机版

扫一扫进手机版
返回顶部