3: 组件间的依赖管理(纯汉语版)
基于Prism库的应用程序是由多个松耦合的类型和服务组成。组件需要和用户动作产生的内容和通知交互。因为组件式松耦合的,组件需要和其他组件交互。要把这些连接起来,Prism使用了依赖注入容器。
依赖注入容器减少对象间的耦合关系,容器通过配置管理对象的实例化,生命周期。对象创建期间,容器注入对象所需的依赖项。如果这些依赖性没被创建,容器就先创建和解析依赖项。一些情况下,容器本身作为一个依赖性被解析。例如,使用Unity作为容器,模块需要注入容器,这样模块才能注册其视图和服务到容器中。
使用容器的诸般好处:
- 容器移除了对象对于其依赖项生命周期的管理。
- 容器允许交换依赖项的实现而不影响组件。
- 容器功能有助于可测性,因为依赖项是可以模拟的。
- 容器提升易维护性。新组件可以轻松添加到系统中。
在基于上述Prism库的应用程序的上下文中,容器有一些特别的优势:
- 当模块加载时,容器将模块依赖项注入给模块。
- 容器可以用来注册和解析视图模型和视图。
- 容器创建视图模型并将其注入给视图。
- 给容器注入组件服务,例如区域管理器和事件聚合器。
- 当程序需要某模块功能时,容器根据需要注册模块服务。
注意:一些示例使用的Untiy,还有一些示例使用的MEF。Prism本身不是必须需要某容器的。你可以用其他容器,例如Castle Windsor, StructureMap, 和Spring.NET。
关键决定:选择一个依赖注入容器
Prism提供了两个选择:Unity或是MEF。Prism是可扩展的,因此花费一点点工作量也可以使用其他容器。尽管工作方式不同,Untiy和MEF都提供了依赖注入的基本功能:
- 都可以利用容器注册类型
- 都可以利用容器注册实例
- 都可以马上创建出注册的类型的实例。
- 都可以注入注册类型的实例到构造器。
- 都可以注入注册类型的实例到属性。
- 都需要声明特性以标记类型和需要管理的依赖项。
- 都在一个对象图中解析依赖项。
Untiy独有功能:
- 可以解析没注册的类型。
- 可以解析开泛型。
- 可以利用截断类获取调用对象并添加额外的功能到调用对象上。
MEF独有功能:
- 在路径中发现程序集。
- 使用XAP文件下载并发现程序。
- 重新组合属性和集合作为新对象被发现。
- 自动导出继承类型。
- 它可以同 .NET Framework一同部署。
容器拥有不同的功能和工作方式。但Prism库可以和它们一同工作并提供类似的功能。当考虑要用那个容器时,考虑它的能力适不适合你的方案。
使用容器的考虑
在使用容器之前你该考虑以下内容:
- 考虑是否适合使用容器注册和解析组件:
- 考虑容器注册和解析的性能是否符合你的方案。例如,如果你需要在一个呈现方法里创建10000个多边形,解析多边形肯定严重影响性能,因为容器会为每个多边形创建一个实体。
- 如果依赖项太多,或是有深层次的依赖关系,创建对象的花销会明显提高
- 如果组件没有任何依赖项,也没必要使用容器。
- 如果组件有一个依赖项就集成在类型里并且不会有什么改变,也没必要将其弄进容器里。
- 考虑组件是否应该被注册成单例:
- 如果组件是一个全局服务,是一个单一资源的管理器,例如日志服务,你需要将其注册为单例。
- 如果组件提供共享状态给多个用户,你需要将其注册为单例。
- 如果对象每次都需要一个新实例作为依赖对象,那注册它为一个非单例。例如,每个视图都需要一个新实例的视图模型。
- 考虑你配置容器使用代码还是配置文件:
- 如果你想集中管理所有不同的服务,那么使用配置文件。
- 如果你想根据条件进行注册特别的服务,那么使用代码配置。
- 如果你有个模块级别的服务,考虑用代码配置,以便只有模块加载后才注册这个服务。
注意 |
---|
例如MEF的容器不能陪在一个配置文件,必须使用代码进行配置。 |
核心方案
容器的使用主要有两个目的,即注册和解析。
注册
在你可以注册依赖项到一个对象之前,此对象类型的依赖项需要被先注册到容器里。注册一个类型通常涉及传递给容器一个接口和一个实现此接口的具体类型。主要有两种方法用于注册类型和对象:通过代码或通过配置文件。具体的方法时多样的。
通常,通过代码有两种方式注册类型和对象到容器中。
- 你可以注册一个类型或一个映射到容器中,在适当的时机,容器将建立一个你指向的实例。
- 你可以注册一个已存在的实例到你的容器中。将其作为一个单例。解析时容器将返回此实例的引用。
Unity容器注册类型
在初始化期间,类型可以注册其他类型,例如视图和服务。注册允许通过容器来提供它们的依赖项,并允许它们可以从其它类型的存取。为做到这点,类型将需要将容器注入到模块构造器。以下代码显示OrderModule 类型(来自命令快速入门)
// OrderModule.cs public class OrderModule : IModule { public void Initialize() { this.container.RegisterType<IOrdersRepository, OrdersRepository>(new ContainerControlledLifetimeManager()); ... } ... }
这取决于您所使用的容器,注册页可以用配置文件,想看这个例子,请看利用配置文件注册模块(来自模块化应用程序开发)
注意 |
---|
相比于在配置中注册,代码注册的好处是只有在模块加载了才注册模块需要的类型。 |
MEF注册类型
MEF使用基于属性的系统来注册类型到容器。其结果是,添加类型到容器很简单:只需要[Export] 特性到需要的类型,如下代码所示。
[Export(typeof(ILoggerFacade))] public class CallbackLogger: ILoggerFacade { }
另一种选择是当使用MEF创建一个类的实例并注册器到容器。代码如下(来自MEF模块化快速入门):
protected override void ConfigureContainer() { base.ConfigureContainer(); // Because we created the CallbackLogger and it needs to // be used immediately, we compose it to satisfy any imports it has. this.Container.ComposeExportedValue<CallbackLogger>(this.callbackLogger); }
注意: |
---|
当使用MEF作为你的容器是,推荐使用特性去注册类型。 |
解析
在一个类型注册之后,它可以被解析或作为依赖注入。当一个类型被解析,容器需要创建一个新实例,先将依赖项注入到次实例。
一般的,当一个对象解析时,以下三种情况之一会发生:
- 如果类型没有被注册,容器会抛出一个异常。
注意 一些容器,包括Unity,允许你在不注册它的情况下解析一个具体类。 - 如果类型被注册成单例,容器返回一个单例实例。如果这是该类型第一次被调用,容器创建它并保持它的引用,以便以后调用。
-
如果类型没用被注册成单例,容器每次都会返回个新值。
注意 默认情况下,MEF类型注册为单例,容器保持此单例的引用。在Untiy中,默认情况类型注册不为单例。
Unity解析实例
下面的代码示例展示了命令快速入门中OrdersEditorView 和OrdersToolBar 视图从容器解析的地方,和怎样将他们联系到区域里。
// OrderModule.cs public class OrderModule : IModule { public void Initialize() { this.container.RegisterType<IOrdersRepository, OrdersRepository>(new ContainerControlledLifetimeManager()); // Show the Orders Editor view in the shell's main region. this.regionManager.RegisterViewWithRegion("MainRegion", () => this.container.Resolve<OrdersEditorView>()); // Show the Orders Toolbar view in the shell's toolbar region. this.regionManager.RegisterViewWithRegion("GlobalCommandsRegion", () => this.container.Resolve<OrdersToolBar>()); } ... }
OrdersEditorViewModel 的构造器包含下面的依赖项(订单存储库和订单命令代理),这些都是在其被解析时注入的。
// OrdersEditorViewModel.cs public OrdersEditorViewModel(IOrdersRepository ordersRepository, OrdersCommandProxy commandProxy) { this.ordersRepository = ordersRepository; this.commandProxy = commandProxy; // Create dummy order data. this.PopulateOrders(); // Initialize a CollectionView for the underlying Orders collection. this.Orders = new ListCollectionView( _orders ); // Track the current selection. this.Orders.CurrentChanged += SelectedOrderChanged; this.Orders.MoveCurrentTo(null); }
除了上面代码展示的构造函数注入,Unity也可以属性注入。任何属性只要有[Dependency] 特性,当对象被解析时可以自动解析并注入。
MEF解析实例
下面代码示例展示Bootstrapper 怎样获得一个壳实例(来自MEF模块快速入门)。不是具体类型的,代码可以需要一个接口的实例(下面不是一个具体类型吗?)。
protected override DependencyObject CreateShell() { return this.Container.GetExportedValue<Shell>(); }
任何MEF解析的类,你也可以使用构造函数注入,如下代码所示(来自MEF模块化入门),ILoggerFacade和 IModuleTracker 被注入了。
[ImportingConstructor] public ModuleA(ILoggerFacade logger, IModuleTracker moduleTracker) { if (logger == null) { throw new ArgumentNullException("logger"); } if (moduleTracker == null) { throw new ArgumentNullException("moduleTracker"); } this.logger = logger; this.moduleTracker = moduleTracker; this.moduleTracker.RecordModuleConstructed(WellKnownModuleNames.ModuleA); } 另一个选择是利用属性入门,如下ModuleTracker 所示(来自MEF快速入门),ILoggerFacade 属性被注入。 C# [Export(typeof(IModuleTracker))] public class ModuleTracker : IModuleTracker { [Import] private ILoggerFacade Logger; }
在Prism中使用依赖注入容器和服务
依赖注入容器,通常简称“容器”,用于满足组件之间的依赖性;满足这些依赖关系通常涉及注册和解析。Prism提供Untiy和MEF支持,但并不是非它俩不可。因为Prism通过IServiceLocator 接口访问容器,所有容器是可以被替换的。未做到这点,你的容器必须实现IServiceLocator 接口。通常,若果你替换容器,你也要提供对应的引导器。IServiceLocator 接口定义在公共服务定位器库。这是一个提供抽象的IoC(控制反转)容器的开源代码,例如依赖组人容器和服务定义器。它的目标是使类库使用控制反转和服务定位模式却不依赖于特别的实现。
Prism库提供UnityServiceLocatorAdapter 和MefServiceLocatorAdapter。这俩适配器都实现了ISeviceLocator 接口并扩展ServiceLocatorImplBaset类型。下图展示了类的继承关系。
尽管Prism不依赖于特别的容器,但一个程序中只能使用一种容器。(然后是一堆废话,意思是示例中使用了某一容器,不代表Prism就非得用这个容器,也可以使用其他容器,Prism就是这么牛逼,写书的你就这么喜欢玩逻辑游戏吗)。
IServiceLocator
下面代码展示了IServiceLocator 接口。
public interface IServiceLocator : IServiceProvider { object GetInstance(Type serviceType); object GetInstance(Type serviceType, string key); IEnumerable<object> GetAllInstances(Type serviceType); TService GetInstance<TService>(); TService GetInstance<TService>(string key); IEnumerable<TService> GetAllInstances<TService>(); }
Prism中扩展了服务定位器,扩展方法的嗲吗如下所示。你可以看到IServiceLocator 用来解析,这意味着它是用来获取示例的;不是用来注册的。
// ServiceLocatorExtensions public static class ServiceLocatorExtensions { public static object TryResolve(this IServiceLocator locator, Type type) { try { return locator.GetInstance(type); } catch (ActivationException) { return null; } } public static T TryResolve<T>(this IServiceLocator locator) where T: class { return locator.TryResolve(typeof(T)) as T; } }
TryResolve 扩展方法——Unity 容器不支持——返回一个已经注册的型的实例;否则它返回null。
ModuleInitializer 会用IServiceLocator 在模块加载期间来解析模块,如下代码所示。
// ModuleInitializer.cs - Initialize() IModule moduleInstance = null; try { moduleInstance = this.CreateModule(moduleInfo); moduleInstance.Initialize(); } ...
// ModuleInitializer.cs - CreateModule() protected virtual IModule CreateModule(string typeName) { Type moduleType = Type.GetType(typeName); if (moduleType == null) { throw new ModuleInitializeException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.FailedToGetType, typeName)); } return (IModule)this.serviceLocator.GetInstance(moduleType); }
使用IServiceLocator注意事项
IServiceLocator 不是意味着它就是通用容器。容器有不同的用法语义,以决定为什么他被选中。牢记这一点,股票操盘程序使用依赖注入容器直接代替使用IServiceLocator。这是一个开发程序的推荐方式。
在以下情况下,你该考虑使用IServiceLocator:
- 你是个独立软件供应商,需要开发一个第三方服务以支持多个容器。
- 你要设计一个服务,需要多个容器来组织。
更多信息
有关容器的更多信息,请看以下:
- MSDN上的Unity Application Block
- CodePlex上的Unity community site
- MSDN上的Managed Extensibility Framework Overview
- CodePlex上的MEF community site
- Martin Fowler的站点上的文章Inversion of Control containers and the Dependency Injection pattern
- MSDN杂志上的Design Patterns: Dependency Injection
- MSDN杂志上的Loosen Up: Tame Your Software Dependencies for More Flexible Apps
- Castle Project
- StructureMap
- Spring.NET