Prism 4 文档 ---第3章 管理组件间的依赖关系

时间:2022-03-02 03:47:46
 基于Prism类库的应用程序可能是由多个松耦合的类型和服务组成的复杂应用程序,他们需要根据用户的动作发出内容和接收通知进行互动,由于他们是松耦合的,他们需要一种方式来互动和交流来传递业务功能的需求。
    为了将这些零散的模块组合在一起,基于Prism的应用程序使用了一个依赖注入容器,依赖注入容器通过基于容器的配置提供实例化类对象并且管理它们的声明周期减少了对象之间的依赖耦合关系。在创建对象时,容器将其所需要的依赖全部注入其中。如果这些依赖还没有被创建,容器将会首先创建并解析依赖所需要的对象或者它的依赖关系。在某些情况下,容器本身也被解析为依赖关系的一部分。例如,当使用Unity作为应用程序的容器时,模块被容器注入,所以他们可以将他们的视图和服务注入到容器中。
    以下是一些使用容器的优势:
  • 容器移除了组件需要去定位它的依赖对象和管理这些对象的生命周期的任务。
  • 容器实现了在不影响组件的情况下改变依赖的实现方法。
  • 容器可以通过模拟依赖组件而使得测试更加容易。
  • 容器通过更容易的添加新组件到系统中增强了应用程序的可维护性。

在使用Prism应用程序中,容器还有一些特殊的优势:

  • 容器在模块被加载时才将模块的依赖关系注入
  • 容器被用来注册和解析视图模型和视图
  • 容器可以创建模块的视图并且注入到视图中
  • 容器注入到组合服务中,例如区域管理和事件聚合器
  • 容器被用来注册模块特有的服务

注意:

    一些Prism示例使用Unity作为应用程序容器,另一些示例代码例如Modularity QuickStarts使用的是MEF,Prism类库本身并使用专用容器的,你可以使用它的服务和模式而使用其他的容器,比如Castle Windsor,StructureMap,和Spring.NET。
    关键决定:选择一个依赖注入容器
    Prism类库提供了两种可选择的依赖注入容器:Unity和MEF,Prism是可以扩展的,所以也可以通过一点代码工作来使用其他的自定义容器。虽然Unity和MEF的工作原理非常不同,但是它们都提供了依赖注入的的基本功能。以下是这两种容器共同点:
  • 它们都可以在容器中注入类型
  • 它们都可以在容器中注入实例
  • 它们都可以创建注册类型的实例
  • 它们都可以在构造方法中进行注入
  • 它们都可以在属性中进行注入
  • 它们都可以通过声明属性来管理那些需要标记的类型和依赖关系
  • 它们都可以在一个对象图表中解析依赖关系
Unity特有的功能:
  • 可以解析未注册的具体类型
  • 可以解析公开的泛型类
  • 使用监听来捕获被注入者调用的内容,并向这些目标对象中添加额外的功能
MEF特有的功能:
  • 在目录中发现程序集
  • 使用下载XAP文件并发现程序集
  • 当发现心类型时,重组属性和集合
  • 自动的导出(发现)子类型
  • 依赖.NET框架

这些容器拥有不同的能力并且工作原理也不相同,但是Prism类库可以工作在这些容器之上并且提供相似的功能。当考虑使用何种容器时,记住上文中所述的特点并决定哪种容器更合适你。

使用容器的注意事项
    在使用容器之前,你需要考虑以下问题:
  • 考虑是否将组件注册/解析到容器中是合适的
    • 考虑在你的场景中,注册到容器和解析实例的性能影响是否可以接受。例如,你需要在当前的视角中创建10000个多边形来绘制某个表面,那么容器接卸这么多的多边形实例的开销将会造成非常大的性能负担,因为它是使用反射的方式来创建每个实例的。
    • 如果依赖关系比较多或者层次比较深,那么创建它们的花费也是比较大
    • 如果组件不依赖任何其他组件或者不被其他组件依赖,那么把它放到容器中将是没有意义的
    • 如果组件拥有单独的一套依赖关系,并且这些依赖关系是完整的并且不会改变,那么将它放到容器中也是没有意义的
  • 根据组件的生命周期来决定它是被注册为一个单例还是注册为一个实例
    • 如果组件是一个全局服务扮演着一个单一资源的管理角色时,就像日至服务,将它们注册为单例形式。
    • 如果组件要在多个用户见保持共享状态,那么注册为单例形式
    • 如果组件在每次注入时都需要一个独立的依赖实例,那么就不能注册为单例形式。例如,每个视图都需要一个视图模型的实例。
  • 考虑你将使用代码还是使用配置文件来配置容器
    • 如果你想集中的管理不同的服务,那么使用配置文件来配置容器
    • 如果你要根据特定的条件注册特定的服务,那么使用代码来配置容器
    • 如果你拥有模块级服务,考虑使用代码来配置容器,因为那样这些服务才会在加载时被注册

注意:

    一些容器,比如MEF,不能使用配置文件来配置容器,只能通过代码方式来配置
核心方案
容器主要用于两个主要目的:注册和解析
注册
    在你将依赖注入到对象之前,你必须向容器中注册对象的依赖的类型。注册一个类型通常涉及到需要向容器提供一个接口和一个实现了该接口的具体类型。注册类型和对象通常有两种主要的方法:通过代码或者配置文件。当然不同的容器也有不同的方法。
    通常,通过代码向容器中注册类型和对象有两种方法:
  • 你可以向容器中注册一个类型或者一张关系图,在适当的时候,容器将会创建你所指定的类的实例
  • 你可以向容器中将一个已经存在的对象注册为一个单例形式。容器将会返回这个已存在对象的引用。
使用Unity容器注册类型

在初始化过程中,一种类型可以注册另外的类型,就像视图和服务。注册后就可以向整个容器提供它们的依赖关系,也可以通过其他类型来访问它们。为了做到这一点,类型需要在模块的构造器中向容器注入。以下代码是Commanding QuickStart示例代码中OrderModule是如何注册一个类型的。

public class OrderModule : IModule
{
public void Initialize()
{
this.container.RegisterType<IOrdersRepository, OrdersRepository>(new ContainerControlledLifetimeManager());
...
}
...
}

取决于你所选取的容器,注册也可以在代码外通过配置文件来实现。例如,参加第4章"Modular Application Development."中的"Registering Modules using a Configuration File"

注意:

与配置文件相比,通过代码来注册的优势在于,当模块被加载时才会注册。

使用MEF注册类型

MEF使用了基于属性的方式向容器中注册类型。因此,向容器中添加类型注册变的非常简单:为类型添加一个[Export]属性,如下代码所示,

[Export(typeof(ILoggerFacade))]
public class CallbackLogger: ILoggerFacade
{
}

使用MEF注册的另一种方法是将一个特定实例注册到容器中,如示例Modularity for Silverlight with MEF QuickStart中的QuickStartBootstrapper中注册函数ConfigureContainer所实现的这样,如下:

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中类型被注册为单例模式,容器将会保留对象的引用。在Unity中,默认情况下将返回新的实例对象,容器不会保留对象的引用
在Unity中解析对象实例

以下代码是Commanding QuickStart实例中的两个视图OrdersEditorViewOrdersToolBar是如何被容器解析,并且放置到其对应的区域中去的。

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),当它们被解析时,它们就被注入了。

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]属性特征的属性都会在对象被解析时被自动解析和进行注入。
在MEF中解析对象实例
以下代码显示了在Modularity for Silverlight with MEF QuickStart实例中Bootstrapper如何获得一个Shell的实例的。代码也可以请求一个接口的实例,而不仅是请求一个实际类型的实例。
protected override DependencyObject CreateShell()
{
return this.Container.GetExportedValue<Shell>();
}
在使用MEF解析的任何类中,你也可以使用构造方法注入,

如实例Modularity for Silverlight with MEF QuickStart中的ModuleA,ILoggerFacadeIModuleTracker就是以如下代码的方式注入的。

[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的所示方法。

[Export(typeof(IModuleTracker))]
public class ModuleTracker : IModuleTracker
{
// Due to Silverlight/MEF restrictions, this must be public.
[Import] public ILoggerFacade Logger;
}
注意:在Silverlight中,imported属性和字段必须为public
 
在Prism中使用依赖注入容器和服务
依赖注入容器,通常被称为“容器”,用来承载组件间的依赖关系;承载这些依赖关系通常涉及到注册和解析。虽然Prism提供了Unity和MEF两种解析容器,但Prism本身并不基于特定容器。因为Prism使用IServiceLocator接口访问这些容器,所以容器是可以被替换的。为达到这一点,所选的容器必须实现IServiceLocator接口,通常来讲,如果你要替换容器,你需要提供自己特定容器的引导程序,IServiceLocator接口被定义在Common Service Locator 类库中。这是一个开源库并尽力尝试提高用一个通过IoC(控制反转)思想的容器的抽象,就像依赖注入容器和服务定位器,使用该服务的目地是在不依赖于某种具体实现的情况下使用IoC和服务定位。
 

Prism 类库提供了UnityServiceLocatorAdapter 和 MefServiceLocatorAdapter. 这两个类都实现了通过扩展ServiceLocatorImplBase实现了ISeviceLocator接口,下面插图展示了类之间的关系Prism 4 文档 ---第3章 管理组件间的依赖关系

Prism 4 文档 ---第3章 管理组件间的依赖关系

Prism 4 文档 ---第3章 管理组件间的依赖关系

虽然Prism类库没有依赖或者实现任何特定的容器,但通常情况下,应用程序会依赖于某个特定的容器。这意味特定的应用程序会依赖某个容器,但是Prism类库并不直接和任何容器发生关系的是合理的。例如Stock Trader RI和一部分的QuickStart使用Prism并且依赖于Unity容器,而另一部分QuickStart和示例则依赖于MEF。

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只用来解析,也就是取得一个实例,而不是用来注册
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在模块的加载过程中解析模块,如下代码所示:

IModule moduleInstance = null;
try
{
moduleInstance = this.CreateModule(moduleInfo);
moduleInstance.Initialize();
}
...
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并不是一个通用的容器。容器有不同的使用语义,这也是为什么选择某个容器的原因。考虑到这一点,Stock Trader RI直接使用了Unity而不是IServiceLocator,这是你的应用程序开发的建议做法。
在下面情况下,比较时候使用IServiceLocator:
  • 你是一个独立软件开发商(ISV),开发一个适用于多容器的第三方服务
  • 你在一个使用多种容器的系统中设计一个服务
更多信息
涉及到跟多的容器相关的信息,请参考如下信息: