本章介绍:
第三章主要介绍了为什么 要选择依赖注入容器,并且通过对比,告诉用户如何选择Prism自带的依赖注入容器(也就是Unity和MEF)。而后,通过讲解依赖注入容器的两个基本操作注册和解析,以及自带容器是如何使用这两个操作,来进一步加深对容器的理解。最后,通过对IServiceLocator的讲解(包含内容讲解和使用事项说明)清楚的描述了应该如何更换属于自己的依赖注入容器以及Prism与依赖注入容器间的关系。
个人强烈建议没有使用过依赖注入容器开发项目经验的认真研讨此章,并且可以去网上查看Unity和MEF的相关资料,虽然Prism本身并不基于容器开发,但是一般Prism应用程序都会选择一个容器以达到IoC的目地。
第三章 管理组件间依赖关系
Prism组件的应用程序有可能是由若干个松耦合的类型和服务组成的复杂应用程序。他们需要发布信息或者响应用户通知。又因为他们之间是松耦合关系,所以他们需要一个与其它类交互所需业务功能的方法。
为了将这些零散的模块组合在起,Prism应用程序使用一个依赖注入容器。依赖注入容器通过提供实例化对象并且管理这些对象的生命周期来降低这些对象与其它对象的耦合度。当一个对象创建时,容器就将它所需要的所有依赖全部注入其中。如果这些依赖没有被创建,那么容器会先创建这些依赖所需要的对象或者它的依赖关系。在一些情况下,容器本身也是依赖关系的一部分。比如使用Unity当作容器时,模块可以注入到容器中(在我理解中是这样的have sth. done语法总感觉翻译有点不对,这里要表达一个容器是被注入方,但又不是被模块注入的观念。)。所以,他们可以将服务和视图注入到容器中。
以下是使用容器的好处:
1、容器去除了组件定位他们的依赖对象和管理这些对象生命周期的任务
2、容器实现了在不影响组件的情况下改变依赖的实现方法
3、容器可通过模拟依赖组件而使测试更加容易
4、容器可以简单的操作来添加组件而保证了应用程序的可维护性
在使用Prism库的应用程序中,容器还有以下优点:
1、容器在模块加载时才将模块的依赖关系注入
2、容器用来注册和解析视图模型和视图
3、容器可以创建视图模型,并且注入到视图中
4、容器用来注册模块特有的服务
【注意】:一部分Prism示例程序基于Unity容器,其它代码如Modularity QuickStarts使用的是MEF。Prism本身并不提供容器,所以你可以使用其它容器来完成这一任务,就好像Castle Windsor,StructureMap,和Spring.NET。
3.1 关键决定:选择一个依赖注入容器
Prism提供了两种依赖注入容器以供选择:Unity或者MEF。因为Prism有可扩展性,所以也可以通过添加一些代码来使用自定义的容器。虽然Unity和MEF的工作原理大相径庭,但都提供了依赖注入容器的基本功能。以下是他们的共同点:
1、他们都可以向容器中注册类型
2、他们都可以向容器中注册实例
3、他们都会产生注册类型的实例
4、他们都可以完成构造函数注入
5、他们都可以完成属性值注入
6、他们都可以通过设置特征(Attribute)来标志哪些类型和依赖关系是需要管理的。
7、他们都在一个object图表中分析依赖关系
Unity特有的功能:
1、可以解析未注册的具体类型
2、可以解析公开的泛型类(比如List<T>这样的公开泛型类)
3、使用监听捕获被注入者调用的内容,并向这些内容中添加功能。
MEF特有的功能:
1、在目录中发现程序集
2、使用下载XAP文件发现程序集
3、在新的类被发现时,重组属性和集合
4、可以自动发现子类
5、依赖于.NET框架
这些容器使用不同的工作原理,也有不同的功能。但是Prism可以工作在这些容器之上并且提供相似的功能。在决定使用哪种容器时,记住上文所述的特点,并且决定哪一种容器更适合于你。
3.2 如何使用容器
在使用容器前,你需要思考以下问题:
1、思考是否有必要将组件注册到容器中:
a) 在你的场景中注册类型和解析实例所花费的性能是否在你的接收范围内。比如,你需要在当前视角中创建1万个多边形来绘制某个表面,那么容器解析这么多多边形实例的开销将会造成非常大的性能负担,因为容器使用反射来创建每一个实例。
b) 如果组件的依赖关系很多或者很复杂,那么创建他们的花费也会很大。
c) 如果一个组件并不依赖于其它组件也不是其它组件的依赖,那么将他放到容器中就是没有意义的。
d) 如果一个组件拥有单独一套依赖关系,并且这些依赖关系是组件中必不可少的组成部分,也不常改变。那么将他放到容器中也没有意义。
2、根据组件的生命周期决定是注册为一个单例还是注册为一个实例:
a) 如果组件是个全局服务担当某一种资源管理的作用时,比如日志服务,将他注册为单例形式。
b) 如果组件要在多用户间共享状态,那么注册为单例形式。
c) 如果组件在每次注入时都需要一个独立的依赖实例,那么就不要注册为单例形式,比如每一个视图都需要一个独立的视图模型的实例。
3、考虑你是需要使用配置文件还是代码来配置容器:
a) 如果你需要集中管理所有不同的服务,那么使用配置文件配置容器。
b) 如果你需要根据情况的注册特定服务,那么使用代码配置容器。
c) 如果你有模块级的服务,那么考虑通过代码配置容器,因为这样这些服务只有在模块被加载后才会被注册。
3.3 重要场景
使用容器的两个主要目地,也就是注册和解析。
3.3.1 注册
在将依赖注入到对象前,必须向容器注册对象的依赖的类型。注册一个类型通常需要向容器提供一个接口,和一个实现该接口的具体类型。注册类型和对象有两种基本方法:通过代码或者通过配置文件。当然一些特别的容器也有其它手段。
通常来说,有两种通过代码注册类型和对象有以下两种方法:
1、你可以向容器注册一个类型和一个关系。在合适的时候,容器会创建你所指定的类的实例
2、你可以注册一个已经存在的对象,并以此作为单例。容器则会返回已经存在对象的引用。
3.3.1.1 使用Unity注册类型
在初始化过程中,一个类型可以注册在另一个类型上,比如视图和服务的关系。注册后就可以向整个容器提供他们的依赖关系,也可以通过其它类型来访问他们。为了达到这一目地,类型需要在模块的构造器中向容器注入。以下代码是Commanding QuickStart示例代码中OrderModule是如何注册一个类型的。
1 public class OrderModule : IModule{public void Initialize(){this.container.RegisterType<IOrdersRepository, OrdersRepository>(new ContainerControlledLifetimeManager());...}...}
根据你所选择的容器,注册也可以在代码外通过配置文件的形式实现。参见第四章“模块化程序开发”中的实例“使用配置文件注册模块”。
【注意】:与使用配置文件注册相比,使用代码注册有在模块被加载后才进行模块的注册内容的优势。
3.3.1.2 使用MEF注册类型
MEF使用基于特征的方式向容器中注册类型。因此,注册类型的操作非常简单:为类型添加一个[Export]特征,就像以下代码所示的:
1 [Export(typeof(ILoggerFacade))]public class CallbackLogger: ILoggerFacade{}
使用MEF注册的另一种方法是将一个特定实例注册到容器中,如示例Modularity for Silverlight with MEF QuickStart中的QuickStartBootstrapper中注册函数ConfigureContainer所实现的这样,如下:
1 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时,更推荐使用属性来注册类型。
3.3.2 解析
当一个类型被注册后,他就可以被当成一个依赖来解析或者注入。当一个类型需要解析时,容器就会创建一个注入实例,并且将它们的依赖注入其中。
通常,当解析发生时,会出现以下三种情况中的一种:
1、如果类型没有被注册,那就会返回一个异常
【注意】:包含Unity在内的一些容器,允许你在不注册的情况下解析具体的类。
2、如果类型已经被注册为一个单例,那么容器就会返回这个单例的实例。如果这是该类型第一次被调用,那容器就会创建这个单例并且保存他以备以后使用。
3、如果类型没有被注册为单例,那么容器就会返回一个新的实例。
【注意】:在默认的设置下,向MEF注册的类型都是单例模式并且容器会保存对象的引用。在Unity中,则默认会返回一个新的实例,容器也不保存这些实例的引用。
3.3.2.1 在Unity中解析实例
以下代码是Commanding QuickStart实例中的两个视图OrdersEditorView和OrdersToolBar是如何被容器解析,并且放置到其对应的区域中去的。
1 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>()); } ...}
OrdersEditorPresentationModel中包含了两个依赖(IOrdersRepository和OrdersCommandProxy),当他们被解析时,注入就发生了。
1 public OrdersEditorPresentationModel( IOrdersRepository ordersRepository, OrdersCommandProxy commandProxy ){ this.ordersRepository = ordersRepository; this.commandProxy = commandProxy; // Create dummy order data. this.PopulateOrders(); // Initialize a CollectionView for the underlying Orders collection.#if SILVERLIGHT this.Orders = new PagedCollectionView( _orders );#else this.Orders = new ListCollectionView( _orders );#endif // Track the current selection. this.Orders.CurrentChanged += SelectedOrderChanged; this.Orders.MoveCurrentTo(null);}
构造函数注入就如以上代码所示,Unity也支持属性值注入。任何拥有[Dependency]特征的属性都会在对象被解析时被自动解析和注入。
3.3.2.2 在MEF中解析实例
以下代码显示了在Modularity for Silverlight with MEF QuickStart实例中Bootstrapper如何获得一个Shell的实例的。代码也可以请求一个接口的实例,而不仅是请求一个实际类型的实例。
1 protected override DependencyObject CreateShell(){ return this.Container.GetExportedValue<Shell>();}
使用MEF解析任何类型时,通常使用构造函数注入。如实例Modularity for Silverlight with MEF QuickStart中的ModuleA,ILoggerFacade和IModuleTracker就是以如下代码的方式注入的。
1 [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);}
除了构造函数注入,也可以选择属性值注入,如Modularity for Silverlight with MEF QuickStart实例中的ModuleTracker类中向实例中注入ILoggerFacade的所示方法。
1 [Export(typeof(IModuleTracker))]public class ModuleTracker : IModuleTracker{ // Due to Silverlight/MEF restrictions, this must be public. [Import] public ILoggerFacade Logger;}
【注意】:在Silverlight中,被MEF注入的属性和字段都必须是公有的。
3.4 在Prism中使用依赖注入容器和服务
依赖注入容器,通常被称为“容器”,用来提供组件间依赖关系的;为了满足这些依赖关系,通常需要使用注册和解析。虽然Prism提供了Unity和MEF两种解析窗口,但Prism本身并不基于特定容器。因为Prism使用IServiceLocator访问这些容器,所以容器是可以被替换的。如果需要替换容器,那么你的容器就需要实现IServiceLocator接口。通常在更换特定容器后,也需要一个专属于容器的启动器。IServiceLocator定义了通用服务定位库。这是一个开源库并且尽力尝试提供一个基于IoC(控制反转)思想的容器(例如依赖注入容器)的抽象和服务定位器。使用该服务的目地是在不依赖于某种具体实现的情况下使用IoC和服务定位。
Prism库提供了UnityServiceLocatorAdapter和MefServiceLocatorAdapter,这些适配器都通过扩展ServiceLocatorImplBase类实现了IServiceLocator接口。
虽然Prism不引用或者依赖任何特定的容器,但是通常应用程序会依赖于某个容器。这也就是应用程序会提及某个容器,而Prism并不直接和这个容器发生关系。举例来说,示例Stock Trader RI和一部分的QuickStart使用Prism并且依赖于Unity容器,而另一部分QuickStart和示例则依赖于MEF。
3.5 IServiceLocator
IServiceLocator如以下代码所示:
1 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只用来解析,也就是取得一个实例,而不是用来注册。
1 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以解析模块,如以下代码所示。
1 IModule moduleInstance = null;try{ moduleInstance = this.CreateModule(moduleInfo); moduleInstance.Initialize();}...
1 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);}
3.6 关于使用IServiceLocator的思考
IServiceLocator并不是设计成一个多目标容器。容器在使用时有多种语义,这也是为什么要讨论使用哪种容器的原因。牢记一点,Stock Trader RI直接使用了Unity而不是使用IServiceLocator,这才是适合应用程序开发的方法。
在以下情况下,可能使用IServiceLocator会更合适:
1、你是一个独立软件开发商(ISV),开发一个适用于多容器的第三方服务
2、你在一个使用多种容器的系统中设计一个服务