类库中的依赖注入






4.97/5 (10投票s)
在类库中使用依赖注入和服务定位器的简单方法
类库中的 IoC
开发框架总是很有趣的。这里有一个小技巧,介绍如何在构建和发布供第三方开发者使用的类时,应用控制反转原则。
背景
在设计框架时,您始终希望为客户端开发者提供独立的接口和类。假设您有一个名为 ModelService
的类,它是一个简单的数据访问类,由名为 SimpleORM
的框架发布。
一旦您划分了职责并隔离了接口,您就设计了一个 ModelService
类,该类(通过组合)使用了其他几个接口:IConnectionStringFactory
、IDTOMapper
、IValidationService
。
您希望将依赖的接口注入到 ModelService
类中,以便能够对其进行适当的测试。这可以通过构造函数注入轻松实现。
public ModelService(IConnectionStringFactory factory, IDTOMapper mapper, IValidationService validationService)
{
this.factory = factory;
this.mapper = mapper;
this.validationService = validationService;
}
当您围绕自己的应用程序构建模块,并且不将这些模块作为独立的类库发布时,经常会使用这种类型的依赖注入。您不必担心实例化 ModelService
类,因为您将从 DI 容器中查询 ModelService
实例。后者将知道 IConnectionStringFactory
、IDTOMapper
、IValidationService
或任何其他绑定。
另一方面,当您为第三方使用发布类时,情况略有不同。您不希望调用者能够将他们想要的任何接口注入到您的类中。此外,您不希望他们担心需要传递到构造函数中的任何接口实现。除了 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")]
上述方法的优点是双重的:它允许您在测试中注入模拟对象,并向框架用户隐藏带有参数的构造函数。
[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>();
}
}
}
我编写了一个典型的 ServiceLocator
类,该类使用 Ninject
作为依赖注入框架。您可以使用任何您想要的 DI 框架,因为这对调用者来说是透明的。如果您关心性能,请查看以下不错的文章,其中包含有趣的评估。另外,请注意 ServiceLocator
类及其相应的接口是内部的。
现在,将对依赖类的直接初始化调用替换为对 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
中定义一个 IModuleLoder
接口,SimpleORM
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月 - 已审阅