在面向对象编程的早期,开发者要面对在应用程序或者类库中创建或检索类的实例的问题。针对这个问题有很多的解决方案。在过去几年中,依赖注入(DI)和控制反转(IoC)在开发者中很流行,并且取代了老的方案,比如Singleton。
Singleton是创建和暴露类实例的一个方便的办法,但是它有几个缺点,从下面代码可以看出,类利用Singleton模式暴露了一个属性。
Figure 1. Singleton Pattern Implementation
public class DataService
{
private static DataService _instance;
public static DataService Instance
{
get
{
return _instance ?? (_instance = new DataService());
}
}
}
关于上面的代码有几个问题需要注意:
1.构造函数是private的,这也就意味着只能通过使用静态的Instance属性来创建实例,我们可以通过把构造函数的访问权限设置为internal甚至是public来改变这个状况。
2.实例是按需创建的,这通常是件好事,但是有时我们想在程序启动的时候实例对象就已经准备好。这里我们可以尽可能早地调用Instance属性来达到此目的。
3.比较烦恼的是,这里没有方法来删除实例对象。在某些特定的环境下这可能导致内存泄露。解决这个问题的一个途径是在类中增加一个Delete方法。
4.实例以外的主要实例属性可以被创建,但每个实例需要不同的访问权限,无论是属性或方法。在这里Instance属性并不允许传入参数到构造函数。
随着一些改进,我们可以把这个模式转变成更具实用性和灵活性。而更好更干净的方法是从我们的实现类中删除上面的模式代码,替代的使用一个外部的对象,就像 缓存一个实例对象,然后在应用程序的任何一个地方都可以使用。此时,IOC容器就派上用场了。控制反转术语的意思是创建和持有实例的动作不再是用户类的责 任了,就像在传统的面向对象编程中。现在相反的则是委托一个外部的容器。当然这不是义务的,被缓存的实例经常被注入到用户类的构造函数中或通过用户类的属 性来使得实例有效。这也就是我们谈论的依赖注入,或者DI。需要注意的是,依赖注入对于IoC容器来说并不是必须的,但却是是一个方便的方法来使用户类和 缓存实例或缓存实例本身解耦。例如,一个经典的应用程序可能是下面代码展示的那样。在这个例子中,程序员准备提供两个service的实现,其中一个用于 实际运行,另一个用于测试。在一些情况下,开发者可能还会提供第三个实现用于满足设计时的数据需求,比如,可能用于Blend或者VS的设计器中。
Figure 2. Classic Composition of Consumer and Service
public class Consumer
{
private IDataService _service;
public Consumer()
{
if (App.IsTestMode)
{
_service = new TestDataService();
}
else
{
_service = new DataService();
}
}
}
public interface IDataService
{
Task<DataItem> GetData();
}
public class DataService : IDataService
{
public async Task<DataItem> GetData()
{
// TODO Provide a runtime implementation
// of the GetData method.
// ...
}
}
public class TestDataService : IDataService
{
public async Task<DataItem> GetData()
{
// TODO Provide a test implementation
// of the GetData method.
// ...
}
}
使用依赖注入,会使代码变得更简洁,就像下面所示。这就是DI的核心原则:在某个地方的一个类,来负责创建service的正确实现,然后注入。
Figure 3. Dependency Injection
public class ConsumerWithInjection
{
private IDataService _service;
public ConsumerWithInjection(IDataService service)
{
_service = service;
}
}
我们需要负责来创建service然后把它注入到用户类中,而这也就是Ioc容器变得有用的地方。
使用一个IoC容器
市面上有很多IoC容器的框架。比如:Unity,StructureMap和Castle Windsor都是很流行的基于.Net的IoC容器,同时在很多平台上都是有效的。在本文中,将会使用MVVM Light的SimpleIoc来演示基于MVVM的应用程序中的IoC容器的实用性。
注解:本文提供的例子显示了使用MVVM Light和SimpleIoc容器的所有的技术细节。
这个例子是个简单的Rss阅读器,从CNN网站上下载最新文章的列表然后显示。此app有两个页面:MainPage显示了一个包含文章和标题的列表,单 击后,迁移到DetailsPage,显示文章的标题和概述,同时也有一个链接到CNN网页的按钮。同一个包中包含了两个版本,一个是面向Windows 8的,另一个是面向Windows Phone 8的。
SimpleIoc,就像名字所显示的那样,是一个相当简单的IoC容器,允许注册然后以一种简单的方式从缓存中获取实例。它同样允许在构造函数中使用依 赖注入组合多个对象。使用SimpleIoc,可以注册IDataService,以及它的实现类和用户类,代码如下所示。
Figure 4. Registering the IDataService and the Consumer
if (App.IsTestMode)
{
SimpleIoc.Default.Register<IDataService, TestDataService>();
}
else
{
SimpleIoc.Default.Register<IDataService, DataService>();
}
SimpleIoc.Default.Register<ConsumerWithInjection>();
上面的代码显示了一个很清晰的代码结构。如果应用程序时在测试模式下,传入一个已经缓存了的TestDataService实例,相反,传入已经缓存了的 DataService。这里需要注意的是,注册类的时候并没有创建任何类的实例,实例化的动作是在有需求的时候才进行。只有在实例确实被要求的时候才创 建。接下来就是创建一个ConsumerWithInjection 实例,如下所示:
Figure 5. Getting the ConsumerWithInjection Instance
public ConsumerWithInjection ConsumerInstance
{
get
{
return SimpleIoc.Default.GetInstance<ConsumerWithInjection>();
}
}
上面代码的属性被调用后,SimpleIoc会运行一下的步骤:
1.检查ConsumerWithInjection实例是否已经在缓存中存在,如果是,返回此实例。
2.如果ConsumerWithInjection实例不存在,检查ConsumerWithInjection的构造函数,它需要一个IDataService的实例。
3.检查IDataService实例在缓存中是否可用,如果是,把它传给ConsumerWithInjection的构造函数。
4.如果IDataService实例在缓存中不可用,缓存它并将它传给ConsumerWithInjection的构造函数。
因为实例被缓存了,所以GetInstance方法可以在应用程序的很多地方执行多次。使用代码3所示的用构造函数注入没有太大的必要,尽管它是一个在组 合对象和对象间接偶的一个不错的方式。GetInstance方法也可以返回一个被键值(keyed)的实例。意思是这个IoC容器可以使用相同的类创建 多个实例,然后通过键来索引。这种方式,IoC容器就扮演了缓存的功能,实例可以在需要的时候创建:当使用一个键来调用GetInstance方 法,IoC容器通过传入的键来检查实例是否已经被缓存,如果没有,在返回前创建实例,然后保存到缓存中以备之后使用。获取特定类的所有实例是可能的,如下 面代码所示。最后一行代码返回一个包含了前面创建的4个实例的IEnumerable<ConsumerWithInjection>。
Figure 6. Getting Keyed Instances, and Getting All the Instances
// Default instance
var defaultInstance = SimpleIoc.Default.GetInstance<ConsumerWithInjection>();
// Keyed instances
var keyed1 = SimpleIoc.Default.GetInstance<ConsumerWithInjection>("key1");
var keyed2 = SimpleIoc.Default.GetInstance<ConsumerWithInjection>("key2");
var keyed3 = SimpleIoc.Default.GetInstance<ConsumerWithInjection>("key3");
// Get all the instances (four)
var allInstances = SimpleIoc.Default.GetAllInstances<ConsumerWithInjection>();
注册类的方法
IoC容器最有意思的特性是一个类被注册用来产生实例的方式。每个IoC容器都有某些特性,用一种独特的方式来注册类。其中的一些使用代码配置,而其他的 一些则可以读取外部XML文件,通过容器,以一种具有很高的适应性的方式来注册类。一些容器允许使用工厂模式。有些,像SimpleIoc,则是更简单和 直接。决定使用哪种容器取决于几个条件,比如团队熟悉的一些特定容器,应用程序要求的特性等等。
注册动作可以在一个中央位置发生(经常被称作service locator),这是一个采取重要决定的地方。比如在所有的服务中什么时候使用测试实现。在应用程序的其他地方注册一些类也是可能(经常是必要的)的。 在一些MVVM应用程序中(特别是基于MVVM Light的app),一个名字为ViewModelLocator的类用来创建和暴露一些应用程序的ViewModel。这是一个很方便的类用来注册服 务类和用户类。事实上,一些ViewModel也可以用IoC容器来注册。大多数情况下,在ViewModelLocator的注册类中只有 ViewModel是长期的,其他可能是临时创建的。在一些页面可以迁移的app中,比如Windows 8 app和Windows Phone 8 app。一些实例可能在页面迁移中从一个页面传入到另一页面。在一些情况下,SimpleIoc用来为键实例做缓存,使实例迁移变得简单。为了让IoC容 器能更简单地和其他容器交换,许多容器(包括MVVM Light的SimpleIoc)使用Common Service Locator实现,这依赖一个通用接口(IServiceLocator)和ServiceLocator类,用于实现抽象的IoC容器。因为 SimpleIoc实现了IServiceLocator,所以我们可以在我们的程序中写类似下面的代码,这和code6的执行方式是相同的。如果在项目 的后期选择其他的容器,只需要替换SimpleIoc类就可以了。
Figure 7. Registering the ServiceLocator
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
// Default instance
var defaultInstance = ServiceLocator.Current.GetInstance<ConsumerWithInjection>();
// Keyed instances
var keyed1 = ServiceLocator.Current.GetInstance<ConsumerWithInjection>("key1");
var keyed2 = ServiceLocator.Current.GetInstance<ConsumerWithInjection>("key2");
var keyed3 = ServiceLocator.Current.GetInstance<ConsumerWithInjection>("key3");
// Get all the instances (four)
var allInstances = ServiceLocator.Current.GetAllInstances<ConsumerWithInjection>();
除了注册类和委托一个实例创建到IoC容器,也可以注册一个工厂表达式。委托(通常是lambda表达式)返回一个实例,因为委托可以包含任意的逻辑,如 果有必要,它也可以执行一些复杂的创建代码或者返回一个在程序其他部分早已创建好的实例。下面代码展示了如何注册一个接受DateTime类型参数的 DataItem构造函数的类。这个构造函数只有在真正调用GetInstance方法的时候才执行(不是在调用Register的时候)。参数会精确显 示代码被第一次调用的时候的时间。之后的调用的GetInstance方法将会显示相同的时间,因为实例已经被创建同时被缓存了。
Figure 8. Registering a Factory
public async void InitiateRegistration()
{
// Registering at 0:00:00
SimpleIoc.Default.Register(() => new DataItem(DateTime.Now));
Debug.WriteLine("Registering at " + DateTime.Now);
await Task.Delay(5000);
// Getting at 0:00:05
var item = ServiceLocator.Current.GetInstance<DataItem>();
Debug.WriteLine("Creating at " + item.CreationTime);
await Task.Delay(5000);
// Getting at 0:00:10. Creation time is still the same
item = ServiceLocator.Current.GetInstance<DataItem>();
Debug.WriteLine("Still the same creation time: " + item.CreationTime);
}
MVVM Light的ViewModelLocator当安装完MVVM Light(msi:http://mvvmlight.codeplex.com)。 在VS提供的工程模版里可以看见一个新的基于MvvmLight的模板。MVVM Light支持所有基于XAML的框架(WPF,Silverlight,Windows Phone, Windows 8),因此相同的经验可以在这些框架里复用。在创建完工程后,在ViewModel文件夹中打开ViewModelLocator.cs文件,代码如下所 示。
Figure 9. ViewModelLocator Static Constructor
static ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
if (ViewModelBase.IsInDesignModeStatic)
{
SimpleIoc.Default.Register<IDataService, Design.DesignDataService>();
}
else
{
SimpleIoc.Default.Register<IDataService, DataService>();
}
SimpleIoc.Default.Register<MainViewModel>();
}
因为MainViewModel在它的构造函数中指定了一个IDataService,SimpleIoc会负责创建和组合ViewModel所需要的实 例。在ViewModelLocator中,MainViewModel 作为一个属性被暴露给外界,使得可以数据绑定到MainPage的DataContext上,同时也可以利用设计期的service实现 (DesignDataService)或者运行时的实现(DataService)。当我们运行程序的时候在页面上可以看见“Welcome to MVVM Light.”,在Blend或者VS设计器中打开MainPage.xaml,我们可以看见“Welcome to MVVM Light [design].”运行时的数据和设计时的数据在此可以很简单地区分。默认的应用是通过两个IDataService的实现和 ViewModelBase.IsInDesignModeStatic属性来触发相对应的service实现。一旦MainViewModel在第一次 被决定,IoC容器就可以创建正确的service,缓存它并且把它传给MainViewModel的构造函数,然后缓存实例。IoC容器同时也支持注销 类。当一些实例已经被创建和缓存了,调用Unregister方法可以从缓存中清除这些实例。如果这些实例没有被应用程序的其他地方引用,它们将会被垃圾 回收器回收。
处理View Services
这篇文件已经讲述过Services,也就是一些类提供一些具有数据和功能的ViewModel。然后,在一些时候,ViewModel也需要其他类型的 service,为了在View中使用一些功能。这种情况下,我们所谈论的就是View Services。两种典型的View Services是NavigationService和DialogService。第一种提供一些导航功能,比如NavigateTo,GoBack 等等。Windows 8 Modern app和Windows Phone会经常使用NavigationService,因为它们是使用页面的导航应用。对于ViewModel来说,DialogService也是 非常有用,因为开发者不想知道消息是如何被显示给用户的。ViewModel仅仅提供了错误信息。设计师负责消息的显示,比如:在状态栏或者自定义的对话 框。DialogService通常会提供一些功能。比如ShowStatus,ShowMessage。ShowError等等。 NavigationService的实现根据不同的平台会有所不同。比如在Windows 8 Modern app中,NavigationService可以是自包含的类,使用当前窗口的Frame来执行具体的导航动作。事实上,同样也是每个页面如何使用内建 的NavigationService来中导航。ViewModel是一个普通的对象,它并不包含这样的内建属性,此时,IoC容器派上用场了。 ViewModelLocator中注册了NavigationService,被缓存在IoC容器中,可以注入到任意一个需要用到它的 ViewModel中,如下所示。
Figure 10. Creating and Registering the NavigationService, and Injecting in the MainViewModel
public class ViewModelLocator
{
static ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
if (ViewModelBase.IsInDesignModeStatic)
{
SimpleIoc.Default.Register<IRssService, Design.DesignRssService>();
}
else
{
SimpleIoc.Default.Register<IRssService, RssService>();
}
SimpleIoc.Default.Register<INavigationService, NavigationService>();
SimpleIoc.Default.Register<MainViewModel>();
}
public MainViewModel Main
{
get
{
return ServiceLocator.Current.GetInstance<MainViewModel>();
}
}
}
public class MainViewModel : ViewModelBase
{
private readonly IRssService _rssService;
private readonly INavigationService _navigationService;
public ObservableCollection<RssItem> Items
{
get;
private set;
}
public MainViewModel(
IRssService rssService,
INavigationService navigationService)
{
_rssService = rssService;
_navigationService = navigationService;
Items = new ObservableCollection<RssItem>();
}
// ...
}