65.9K
CodeProject 正在变化。 阅读更多。
Home

Microsoft Unity

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.66/5 (28投票s)

2015年5月10日

CPOL

9分钟阅读

viewsIcon

48001

downloadIcon

1141

对 Microsoft Unity 的初步了解。

引言

本文档初步介绍了 Unity — Microsoft 的依赖注入容器。

什么是依赖注入?为什么它是个好主意?

考虑这样一个经典场景:一个业务对象需要能够将自身持久化到数据库。组织代码的最佳方式是什么,以便业务对象能够访问数据库?

可能性包括:

  1. 在业务对象中硬编码连接字符串。
  2. 将连接字符串放在配置文件中,并将配置文件的位置硬编码到对象中。
  3. 通过构造函数或属性设置器将定位器接口传递给业务对象。业务对象通过定位器请求访问数据库。
  4. 通过构造函数或属性设置器将数据库接口传递给业务对象。

前两种选项将硬编码的配置信息放入业务对象,因此不推荐。

第三种选项是服务定位器模式的一个例子。如果业务对象使用默认服务,则业务对象不再包含任何硬编码的配置信息。然而,有些人认为服务定位器模式是一种反模式,因为(i)业务对象可能按名称请求特定服务,在这种情况下,业务对象必须了解其环境,并且(ii).NET 服务定位器可以提供多种服务类型,因此业务对象对数据库的依赖性被隐藏了;通过查看对象的​​方法签名,这种依赖性并不明显——如果你不愿意仔细检查业务对象的源代码,这会使重构变得困难。

最后一种选项是依赖注入的一个例子。数据库是业务对象的依赖项,数据库被“注入”到业务对象中。依赖注入的优点是业务对象不再关心如何获取数据库对象的任何细节。其唯一目的是执行业务逻辑。即,设计体现了“关注点分离”(组件应该只做一件事),这被认为是良好的编程实践。此外,业务对象的依赖项在构造函数中声明,因此对外部世界可见。

创建数据库对象并将其传递给业务对象的逻辑在哪里?在一个小型应用程序中,开发人员可能会编写一些代码,这些代码会

  1. 查找配置文件以获取连接字符串
  2. 创建数据库对象,以及
  3. 创建业务对象,将其数据库对象通过构造函数传递给它。

Unity 框架取代了这些手工编写的代码,提供了一个可配置的框架,该框架使用声明性语句来定义如何创建和初始化对象。

控制反转

开发人员在上一节中描述的自定义框架和 Unity 都是控制反转(IoC)模式的示例。在这两种情况下,框架在启动时开始执行,并且业务对象由框架实例化并在响应事件时由框架使用,即,框架“控制”业务对象,这与“正常”控制流相反。

服务定位器

可以通过将容器包装在 UnityServiceLocatorAdapter 中,将 Unity 容器转换为服务定位器。尽管一些评论者认为它是一种反模式,Prism 使用 IServiceLocator 接口,因此在 Prism/Unity 应用程序中经常会看到以下代码。

    IUnityContainer container; 
	
    ...
	
    container.RegisterInstance<IServiceLocator>(new UnityServiceLocatorAdapter(container));

注册和解析类型

以下代码片段使用从 Unity 框架获得的 IOptionPricer 接口来为期权定价。

定义 BinomialOptionPricer 对象并将其注册到 UnityContainer,这样,每当容器被要求提供 IOptionPricer 接口时,Unity 容器就会创建并返回 BonomialOptionPricer 的实例。(可以说 IOptionPricer 接口被解析为 BinomialOptionPricer 类型)。

    // This is the interface requested from Unity and used to price the option.
    interface IOptionPricer
    {
        double Calculate(OptionType optionType, double S, double K, double T, double r, double vol);
    }
    
    // This is the object that will be actually returned by Unity 
    // (once registered) whenever an IOptionPricer is requested.
    class BinomialOptionPricer : IOptionPricer
    {
        ...
    }
	
    IUnityContainer container = new UnityContainer(); // Always use the IUnityContainer 
    				// rather than the underlying container.
    container.RegisterType<IOptionPricer, BinomialOptionPricer>();   // Registered 
    				// the default implementation for IOptionPricer.

    ...
	
    // Create a pricer () - request a IOptionPricer
    IOptionPricer pricer = container.Resolve<IOptionPricer>();  // Returns an instance 
								// of BinomialOptionPricer.
	
    // Use the pricer to price an option.
    double PV = price->Calculate(OptionType.Call, S, K, T, r, vol);

单例

可以使用 IUnityContainer.RegisterInstance() 方法配置 Unity 容器,以便在每次请求特定接口时返回相同的对象(单例)。

    IOptionPricer pricer = new BinomialOptionPricer();  // This is always 
				//returned when an IOptionPricer is requested.

    container.RegisterInstance<IOptionPricer>(pricer);  

智能注入

下面的代码片段实例化了一个 OptionPriceFrm 对象。请注意,OptionPriceFrm 构造函数需要两个参数,因此需要有效的 IRateProviderIOptionPricer 接口。传统的代码需要先创建作为参数的对象,然后才能创建窗体,如下所示。

    public class OptionPricerFrm 
    {
        public OptionPricerFrm(IRatesProvider provider, 
		IOptionPricer pricer)  // constructor takes parameter
        {
            ...
        }
        ...
    }	

    IRatesProvider provider = new RatesProvider(...);  // create parameter 1.
	
    IOptionPricer pricer = new OptionPricer(...);  // create parameter 2.
	
    var frm = new OptionPricerFrm(provider, pricer);  // previously constructed objects passed to ctor.

相比之下,Unity 不需要我们为构造函数提供参数值;它将递归地解析构造函数作为参数所需的任何接口。创建窗体的 Unity 代码将如下所示:

    // Unity figures out what parameters are needed to pass to the ctor and creates them.
    var frm = container.Resolve<OptionPricerFrm>(); 

Unity 版本代码的重构更加容易。(例如,如果 OptionPriceFrm 对象被修改,使其构造函数接受一个额外的接口,则无需更改解析 IOptionPricer 接口的任何代码)。

Unity 还提供了覆盖构造函数参数值以及使用属性初始化已解析对象的机制。有关更多详细信息,请参阅 Unity 文档(以及源代码)。

命名注册

同一个接口在应用程序的不同部分可能引用不同的底层对象类型。例如,交易应用程序可能使用 IOptionPricer 接口来为期权定价。交易员可能希望使用非标准模型来对冲其头寸,因为他认为该模型更准确地反映了市场价格,但使用批准的模型来计算限额、费用以及向中间部门报告损益。

以下代码显示了 IOptionPricer 接口的两个独立注册;传递给 RegisterType 方法的参数是该注册的名称。

    IUnityContainer container = new UnityContainer(); // Always use the 
				// IUnityContainer rather than the underlying container.
	
    container.RegisterType<IOptionPricer, 
    ApprovedOptionPricer>("std");       	// Registered the approved option pricer.
    container.RegisterType<IOptionPricer, 
    BinomialOptionPricer>("traders");       // Registered the non-standard binomial pricer.  

要解析类型的注册名称作为参数传递给 Resolve 方法,如下所示。

    IUnityContainer container = new UnityContainer(); // Always use the 
			// IUnityContainer rather than the underlying container.
	
    var approvedPricer = container.Resolve<IOptionPricer>
    	("std");       // Registered the approved option pricer.
	
    var tradersPricer = container.Resolve<IOptionPricer>
    	("traders");    // Registered the non-standard binomial pricer.  

如果未指定名称,则该注册称为默认注册。

请注意,即使已解析对象的构造函数接受不同数量的参数,也可以动态更改注册名称。

使用 Unity 配置文件

如果我们依赖于以前的编码方法并想使用不同的实现类来代替 IOptionPricer,我们需要重新编译应用程序。

Unity 容器可以从 app.config (或 web.config)文件中初始化。在下面的示例中,typemapTo 字符串的第一部分是类型名称,第二部分是对象或接口所在的程序集的名称。该节表示有一个名为“main”的单个 Unity 容器,具有两个从 OptionPricersInterfaces.IOptionPricer 接口到 QuantLib 程序集中的 QuantLib.OptionPricer 对象,以及另一个到 TraderModels 程序集中的 TraderModels.BinomialOptionPricer 对象的映射。

    <unity  xmlns="http://schemas.microsoft.com/practices/2010/unity">
        <containers>
            <container name="main">
	  
                <type name="Traders Screen" 
                      type="OptionPricersInterfaces.IOptionPricer, OptionPricersInterfaces" 
                      mapTo="TraderModels.BinomialOptionPricer, TradersModels"/>
			  
                <type name="Profit and Loss Report" 
                      type="OptionPricersInterfaces.IOptionPricer, OptionPricersInterfaces" 
                      mapTo="QuantLib.OptionPricer, QuantLib"/>
			  
            </container>
        </containers>
    </unity>

下面显示了从配置文件初始化 Unity 容器的代码。

    IUnityContainer container = new UnityContainer();

    // Use configuration file to register types.
    UnityConfigurationSection configSection = 
    (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
    configSection.Configure(container, "main");

模块

Microsoft 提供了许多机制来协助接口和实现的注册,包括模块和 Unity Bootstrapper。模块比 Bootstrapper 更底层,Bootstrapper 在内部使用模块。应用程序倾向于使用 UnityBootstrapper,但了解直接使用模块所涉及的步骤很有用(如下所示)。请注意,模块是 Prism 的一部分,而不是 Unity 的一部分。

    // This is used by the ModuleManager so cannot be omitted.
    container.RegisterInstance<IServiceLocator>(new UnityServiceLocatorAdapter(container));	

    // This is used by the ModuleManager so cannot be omitted.
    TextLogger logger = new TextLogger(); // Default logger
    container.RegisterInstance<ILoggerFacade>(logger);
	
    //
    // Setup the module catalog. Catalog specific logic goes here.	
    //
    IModuleCatalog catalog = 
    new ModuleCatalog();  // Create the catalog - may be a derived type.
    container.RegisterInstance<IModuleCatalog>
    	(catalog);  // Singleton so IModuleManager can find it.	   
    ... 
	
    container.RegisterType<IModuleInitializer, ModuleInitializer>();

    container.RegisterType<IModuleManager, ModuleManager>();
            
    IModuleManager manager = container.Resolve<IModuleManager>();
    manager.Run();

在任何情况下,都必须设置 ModuleCatalog 或派生对象,以便用 ModuleInfo 填充它。有几种 ModuleCatalog 派生类,包括从配置文件填充目录的 ConfigurationModuleCatalog,以及通过扫描目录中的模块来填充目录的 DirectoryModuleCatalog。自定义 ModuleCatalog 对象(实现 IModuleCatalog)也是可能的。设置好目录后,ModuleManager.Run 会加载指定的类型并将其注册到 Unity。

DirectoryModuleCatalog

DirectoryModuleCatalog 指定了启动时要扫描的目录。ModuleManager(仅反射)加载它找到的所有程序集,并检查实现 IModule 的对象。如果找到一个,则创建一个类的实例(在下面的示例中为 Registration)。构造函数(或 IUnityContainer 属性)将被传递一个 IUnityContainer 接口,因为它需要进行注册。创建对象后,将调用 IModule.Initialise() 方法,该方法将接口和实现注册到 Unity。

    public class Registration : IModule
    {
        IUnityContainer container = null;
        public Registration(IUnityContainer container)
        {
            this.container = container;
        }
        
        public void Initialize()
        {
            container.RegisterType<IOptionPricer, OptionPricer>("std");
            container.RegisterType<IOptionPricer, 
            	BinomialTreeOptionPricer>("binomial");
            container.RegisterType<IRatesProvider, RatesProvider>("std");
        }
    }

ConfigurationModuleCatalog

从应用程序(或 Web)配置文件初始化 Unity 的 ConfigurationModuleCatalog 设置起来非常简单。

    ConfigurationModuleCatalog catalog = new ConfigurationModuleCatalog();
    container.RegisterInstance<imodulecatalog>(catalog);

UnityBootstrapper

Microsoft 提供了 UnityBootstrapper 类来简化使用 Unity 的 Prism 应用程序的编码。它所做的不仅仅是初始化 Unity IoC 容器。作者认为 PrismBootstrapperForUnity 会是更好的名字——UnityBootstrapper.Run 方法中会发生以下处理:

  1. 创建 ILoggerFacade 实现。
  2. 通过 CreateModuleCatalog 创建 ModuleCatalog
  3. 创建 Unity 容器。
  4. 配置默认区域适配器映射。
  5. 配置默认区域行为。
  6. 注册 Prism 框架异常。
  7. 通过 CreateShell 创建 Shell。
  8. 初始化 Shell。
  9. 初始化模块并将接口和实现注册到 Unity。

UnityBootstrapper通常通过重载 CreateModuleCatalog()CreateShell() 方法来使用。

    class OptionPricerBootstrapper : UnityBootstrapper
    {
        protected override IModuleCatalog CreateModuleCatalog()  // previously GetModuleCatalog()
        {
            string path = @".\Modules";
            if (!Directory.Exists(path))
                throw new Exception(path + " does not exist.");
            return (new DirectoryModuleCatalog() { ModulePath = path });
        }
        protected override System.Windows.DependencyObject CreateShell()
        {
            MainWindow wnd = new MainWindow(Container);
            wnd.Show();
            return wnd;
        }
    }

    Bootstrapper().Run();

CreateModuleCatalog()通常用于创建指向自注册模块位置的 DirectoryModuleCatalog(如上所示),但也可以轻松返回 ConfigurationModuleCatalog 或自定义 ModuleCatalog

CreateShell() 方法期望 Shell(主窗口)由该方法创建并返回(应删除 App.xml 中的 StartupUri)。请参阅 Prism 文档,了解 Shell 在 Prism(复合)应用程序中的作用。

示例代码

示例代码包含几个项目:

  • OptionPricerInterfaces - 包含所有其他项目使用的 IOptionPricerIRateProviderIOptionPricer 接口的定义。
  • OptionPricers - 包含所有应用程序使用的 IOptionPricerIRateProviderIOptionPricer 的实现。
  • BareBones - 一个非常简单的 Unity 应用程序,用于演示注册和类型解析的基本概念。它是一个命令行应用程序。
  • BareBones2 - 演示了一些稍微复杂一些的 Unity 用法示例。也是一个命令行应用程序。
  • OptionPricerApp - 一个 Winforms 应用程序,显示期权定价对话框,并使用 Unity 在代码中注册和实例化 OptionPricer 对象。
  • OptionPricerApp2 - 一个 Winforms 应用程序,显示期权定价对话框,并使用 Unity 解析 OptionPricerFrmOptionPricerRatesProvider 对象。OptionPricer 接口/实现使用配置文件进行注册。
  • OptionPricerApp3 - 一个 Winforms 应用程序,显示期权定价对话框,并使用 DirectoryModuleCatalog 通过扫描目录中的模块来初始化 Unity 容器。
  • OptionPricerApp4 - 一个使用 Prism UnityBootstrapper 的 WPF 应用程序。

Unity 的潜在问题

大型配置文件维护起来可能很麻烦——很难发现错误。(作者曾在这上面浪费了很多时间)。“新”技术总是非常诱人。没有理由不在整个应用程序中使用 Unity,但如果注册类型很有可能发生变化,那么最好只将注册声明移到配置文件中。作者意识到大型企业中痛苦的发布流程和内部 IT 开发策略可能意味着这些建议并不现实。

每个用例可能都有自己的命名接口注册,以避免一个应用程序部分的变化导致其他部分发生意外变化的情况。

结论

Unity 是一个很棒的轻量级容器,支持依赖注入。无论您使用的是 WPF、Winforms 还是其他任何技术,都没有明显理由不使用它。

历史

暂无更新。

© . All rights reserved.