在类库里注入依赖
在类库里注入依赖
在你的类库中使用依赖注入和服务器定位的一个简单的方式。
类库中的控制反转
框架的开发总是非常有趣的。下面是如何使用控制反转原则为第三方开发者构建并发布类的一个快速提示。
背景
当你设计框架时,总是要为客户端开发人员提取独立的接口和类的。现在你有一个简单数据访问类 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)。