控制反转
同义词 依赖注入一般指控制反转控制反转(Inversion of Control,英文缩写为IoC)是框架的重要特征,并非面向对象编程的专用术语。它与依赖注入(Dependency Injection,简称DI)和依赖查找(Dependency Lookup)并没有关系。
中文名 控制反转 外文名 Inverse of Control 起源时间 1988年 目 的 描述框架的重要特征
目录
1 起源
2 设计模式
3 优缺点
4 实现初探
5 类型
6 实现策略
7 实现方式
起源
早在1988年,Ralph E. Johnson & Brian Foote在论文Designing Reusable Classes中写到:
One important characteristic of a framework is that the methods defined by the user to tailor the framework will often be called from within the framework itself, rather than from the user's application code.
The framework often plays the role of the main program in coordinating and sequencing application activity.
This inversion of control gives frameworks the power to serve as extensible skeletons. The methods supplied by the user tailor the generic algorithms defined in the framework for a particular application.
《设计模式》至少两次使用了控制反转,[1.6.7设计应支持变化]和[5.10模板方法模式]。[1]
2004年,Martin Fowler在其著名文章Inversion of Control Containers and the Dependency Injection pattern中[2] ,使用了该术语。
但是,这些使用案例也使得IoC的含义变得含混。
设计模式
IoC可以认为是一种全新的设计模式,但是理论和时间成熟相对较晚,并没有包含在GoF中。
Interface Driven Design接口驱动,接口驱动有很多好处,可以提供不同灵活的子类实现,增加代码稳定和健壮性等等,但是接口一定是需要实现的,也就是如下语句迟早要执行:AInterface a = new AInterfaceImp(); 这样一来,耦合关系就产生了,如:
classA
{
AInterface a;
A(){}
AMethod()//一个方法
{
a = new AInterfaceImp();
}
}
Class A与AInterfaceImp就是依赖关系,如果想使用AInterface的另外一个实现就需要更改代码了。当然我们可以建立一个Factory来根据条件生成想要的AInterface的具体实现,即:
InterfaceImplFactory
{
AInterface create(Object condition)
{
if(condition == condA)
{
return new AInterfaceImpA();
}
else if(condition == condB)
{
return new AInterfaceImpB();
}
else
{
return new AInterfaceImp();
}
}
}
表面上是在一定程度上缓解了以上问题,但实质上这种代码耦合并没有改变。通过IoC模式可以彻底解决这种耦合,它把耦合从代码中移出去,放到统一的XML 文件中,通过一个容器在需要的时候把这个依赖关系形成,即把需要的接口实现注入到需要它的类中,这可能就是“依赖注入”说法的来源了。
IoC模式,系统中通过引入实现了IoC模式的IoC容器,即可由IoC容器来管理对象的生命周期、依赖关系等,从而使得应用程序的配置和依赖性规范与实际的应用程序代码分开。其中一个特点就是通过文本的配置文件进行应用程序组件间相互关系的配置,而不用重新修改并编译具体的代码。
当前比较知名的IoC容器有:Pico Container、Avalon 、Spring、JBoss、HiveMind、EJB等。
在上面的几个IoC容器中,轻量级的有Pico Container、Avalon、Spring、HiveMind等,超重量级的有EJB,而半轻半重的有容器有JBoss,Jdon等。
可以把IoC模式看做是工厂模式的升华,可以把IoC看作是一个大工厂,只不过这个大工厂里要生成的对象都是在XML文件中给出定义的,然后利用Java 的“反射”编程,根据XML中给出的类名生成相应的对象。从实现来看,IoC是把以前在工厂方法里写死的对象生成代码,改变为由XML文件来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性。
IoC中最基本的Java技术就是“反射”编程。反射又是一个生涩的名词,通俗的说反射就是根据给出的类名(字符串)来生成对象。这种编程方式可以让对象在生成时才决定要生成哪一种对象。反射的应用是很广泛的,像Hibernate、Spring中都是用“反射”做为最基本的技术手段。
在过去,反射编程方式相对于正常的对象生成方式要慢10几倍,这也许也是当时为什么反射技术没有普遍应用开来的原因。但经SUN改良优化后,反射方式生成对象和通常对象生成方式,速度已经相差不大了(但依然有一倍以上的差距)。
优缺点
IoC最大的好处是什么?因为把对象生成放在了XML里定义,所以当我们需要换一个实现子类将会变成很简单(一般这样的对象都是实现于某种接口的),只要修改XML就可以了,这样我们甚至可以实现对象的热插拔(有点像USB接口和SCSI硬盘了)。
IoC最大的缺点是什么?(1)生成一个对象的步骤变复杂了(事实上操作上还是挺简单的),对于不习惯这种方式的人,会觉得有些别扭和不直观。(2)对象生成因为是使用反射编程,在效率上有些损耗。但相对于IoC提高的维护性和灵活性来说,这点损耗是微不足道的,除非某对象的生成对效率要求特别高。(3)缺少IDE重构操作的支持,如果在Eclipse要对类改名,那么你还需要去XML文件里手工去改了,这似乎是所有XML方式的缺陷所在。
实现初探
IOC关注服务(或应用程序部件)是如何定义的以及他们应该如何定位他们依赖的其它服务。通常,通过一个容器或定位框架来获得定义和定位的分离,容器或定位框架负责:
保存可用服务的集合
提供一种方式将各种部件与它们依赖的服务绑定在一起
为应用程序代码提供一种方式来请求已配置的对象(例如,一个所有依赖都满足的对象), 这种方式可以确保该对象需要的所有相关的服务都可用。
类型
现有的框架实际上使用以下三种基本技术的框架执行服务和部件间的绑定:
类型1 (基于接口): 可服务的对象需要实现一个专门的接口,该接口提供了一个对象,可以重用这个对象查找依赖(其它服务)。早期的容器Excalibur使用这种模式。
类型2 (基于setter): 通过JavaBean的属性(setter方法)为可服务对象指定服务。HiveMind和Spring采用这种方式。
类型3 (基于构造函数): 通过构造函数的参数为可服务对象指定服务。PicoContainer只使用这种方式。HiveMind和Spring也使用这种方式。
实现策略
IoC是一个很大的概念,可以用不同的方式实现。其主要形式有两种:
◇依赖查找:容器提供回调接口和上下文条件给组件。EJB和Apache Avalon 都使用这种方式。这样一来,组件就必须使用容器提供的API来查找资源和协作对象,仅有的控制反转只体现在那些回调方法上(也就是上面所说的 类型1):容器将调用这些回调方法,从而让应用代码获得相关资源。
◇依赖注入:组件不做定位查询,只提供普通的Java方法让容器去决定依赖关系。容器全权负责的组件的装配,它会把符合依赖关系的对象通过JavaBean属性或者构造函数传递给需要的对象。通过JavaBean属性注射依赖关系的做法称为设值方法注入(Setter Injection);将依赖关系作为构造函数参数传入的做法称为构造器注入(Constructor Injection)
实现方式
实现数据访问层
数据访问层有两个目标。第一是将数据库引擎从应用中抽象出来,这样就可以随时改变数据库—比方说,从微软SQL变成Oracle。不过在实践上很少会这么做,也没有足够的理由未来使用实现数据访问层而进行重构现有应用的努力。[3]
第二个目标是将数据模型从数据库实现中抽象出来。这使得数据库或代码开源根据需要改变,同时只会影响主应用的一小部分——数据访问层。这一目标是值得的,为了在现有系统中实现它进行必要的重构。
模块与接口重构
依赖注入背后的一个核心思想是单一功能原则(single responsibility principle)。该原则指出,每一个对象应该有一个特定的目的,而应用需要利用这一目的的不同部分应当使用合适的对象。这意味着这些对象在系统的任何地方都可以重用。但在现有系统里面很多时候都不是这样的。[3]
随时增加单元测试
把功能封装到整个对象里面会导致自动测试困难或者不可能。将模块和接口与特定对象隔离,以这种方式重构可以执行更先进的单元测试。按照后面再增加测试的想法继续重构模块是诱惑力的,但这是错误的。[3]
使用服务定位器而不是构造注入
实现控制反转不止一种方法。最常见的办法是使用构造注入,这需要在对象首次被创建是提供所有的软件依赖。然而,构造注入要假设整个系统都使用这一模式,这意味着整个系统必须同时进行重构。这很困难、有风险,且耗时。
========
依赖注入原理(为什么需要依赖注入)
http://blog.csdn.net/coderder/article/details/51897721目录(?)[-]
0 前言
1 为什么需要依赖注入
2 依赖注入的实现方式
21 构造函数注入Contructor Injection
22 setter注入
23 接口注入
3 最后
参考
0. 前言
在软件工程领域,依赖注入(Dependency Injection)是用于实现控制反转(Inversion of Control)的最常见的方式之一。本文主要介绍依赖注入原理和常见的实现方式,重点在于介绍这种年轻的设计模式的适用场景及优势。
1. 为什么需要依赖注入
控制反转用于解耦,解的究竟是谁和谁的耦?这是我在最初了解依赖注入时候产生的第一个问题。
下面我引用Martin Flower在解释介绍注入时使用的一部分代码来说明这个问题。
public class MovieLister {
private MovieFinder finder;
public MovieLister() {
finder = new MovieFinderImpl();
}
public Movie[] moviesDirectedBy(String arg) {
List allMovies = finder.findAll();
for (Iterator it = allMovies.iterator(); it.hasNext();) {
Movie movie = (Movie) it.next();
if (!movie.getDirector().equals(arg)) it.remove();
}
return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
}
...
}
public interface MovieFinder {
List findAll();
}
我们创建了一个名为MovieLister的类来提供需要的电影列表,它moviesDirectedBy方法提供根据导演名来搜索电影的方式。真正负责搜索电影的是实现了MovieFinder接口的MovieFinderImpl,我们的MovieLister类在构造函数中创建了一个MovieFinderImpl的对象。
目前看来,一切都不错。但是,当我们希望修改finder,将finder替换为一种新的实现时(比如为MovieFinder增加一个参数表明Movie数据的来源是哪个数据库),我们不仅需要修改MovieFinderImpl类,还需要修改我们MovieLister中创建MovieFinderImpl的代码。
这就是依赖注入要处理的耦合。这种在MovieLister中创建MovieFinderImpl的方式,使得MovieLister不仅仅依赖于MovieFinder这个接口,它还依赖于MovieListImpl这个实现。 这种在一个类中直接创建另一个类的对象的代码,和硬编码(hard-coded strings)以及硬编码的数字(magic numbers)一样,是一种导致耦合的坏味道,我们可以把这种坏味道称为硬初始化(hard init)。同时,我们也应该像记住硬编码一样记住,new(对象创建)是有毒的。
Hard Init带来的主要坏处有两个方面:1)上文所述的修改其实现时,需要修改创建处的代码;2)不便于测试,这种方式创建的类(上文中的MovieLister)无法单独被测试,其行为和MovieFinderImpl紧紧耦合在一起,同时,也会导致代码的可读性问题(“如果一段代码不便于测试,那么它一定不便于阅读。”)。
2. 依赖注入的实现方式
依赖注入其实并不神奇,我们日常的代码中很多都用到了依赖注入,但很少注意到它,也很少主动使用依赖注入进行解耦。这里我们简单介绍一下赖注入实现三种的方式。
2.1 构造函数注入(Contructor Injection)
这是我认为的最简单的依赖注入方式,我们修改一下上面代码中MovieList的构造函数,使得MovieFinderImpl的实现在MovieLister类之外创建。这样,MovieLister就只依赖于我们定义的MovieFinder接口,而不依赖于MovieFinder的实现了。
public class MovieLister {
private MovieFinder finder;
public MovieLister(MovieFinder finder) {
this.finder = finder;
}
...
}
2.2 setter注入
类似的,我们可以增加一个setter函数来传入创建好的MovieFinder对象,这样同样可以避免在MovieFinder中hard init这个对象。
public class MovieLister {
s...
public void setFinder(MovieFinder finder) {
this.finder = finder;
}
}
2.3 接口注入
接口注入使用接口来提供setter方法,其实现方式如下。
首先要创建一个注入使用的接口。
public interface InjectFinder {
void injectFinder(MovieFinder finder);}
之后,我们让MovieLister实现这个接口。
class MovieLister implements InjectFinder {
...
public void injectFinder(MovieFinder finder) {
this.finder = finder;
}
...
}
========
浅谈依赖注入
http://www.cnblogs.com/yangecnu/p/Introduce-Dependency-Injection.html最近几天在看一本名为Dependency Injection in .NET 的书,主要讲了什么是依赖注入,使用依赖注入的优点,以及.NET平台上依赖注入的各种框架和用法。在这本书的开头,讲述了软件工程中的一个重要的理念就是关注分离(Separation of concern, SoC)。依赖注入不是目的,它是一系列工具和手段,最终的目的是帮助我们开发出松散耦合(loose coupled)、可维护、可测试的代码和程序。这条原则的做法是大家熟知的面向接口,或者说是面向抽象编程。
关于什么是依赖注入,在Stack Overflow上面有一个问题,如何向一个5岁的小孩解释依赖注入,其中得分最高的一个答案是:
“When you go and get things out of the refrigerator for yourself, you can cause problems. You might leave the door open, you might get something Mommy or Daddy doesn’t want you to have. You might even be looking for something we don’t even have or which has expired.
What you should be doing is stating a need, “I need something to drink with lunch,” and then we will make sure you have something when you sit down to eat.”
映射到面向对象程序开发中就是:高层类(5岁小孩)应该依赖底层基础设施(家长)来提供必要的服务。
编写松耦合的代码说起来很简单,但是实际上写着写着就变成了紧耦合。
使用例子来说明可能更简洁明了,首先来看看什么样的代码是紧耦合。
1 不好的实现
编写松耦合代码的第一步,可能大家都熟悉,那就是对系统分层。比如下面的经典的三层架构。
Classic 3-tier architecture
分完层和实现好是两件事情,并不是说分好层之后就能够松耦合了。
1.1 紧耦合的代码
有很多种方式来设计一个灵活的,可维护的复杂应用,但是n层架构是一种大家比较熟悉的方式,这里面的挑战在于如何正确的实现n层架构。
假设要实现一个很简单的电子商务网站,要列出商品列表,如下:
product list page
下面就具体来演示通常的做法,是如何一步一步把代码写出紧耦合的。
1.1.1 数据访问层
要实现商品列表这一功能,首先要编写数据访问层,需要设计数据库及表,在SQLServer中设计的数据库表Product结构如下:
Product Table
表设计好之后,就可以开始写代码了。在Visual Studio 中,新建一个名为DataAccessLayer的工程,添加一个ADO.NET Entity Data Model,此时Visual Studio的向导会自动帮我们生成Product实体和ObjectContext DB操作上下文。这样我们的 Data Access Layer就写好了。
Product Entity Model
1.1.2 业务逻辑层
表现层实际上可以直接访问数据访问层,通过ObjectContext 获取Product 列表。但是大多数情况下,我们不是直接把DB里面的数据展现出来,而是需要对数据进行处理,比如对会员,需要对某些商品的价格打折。这样我们就需要业务逻辑层,来处理这些与具体业务逻辑相关的事情。
新建一个类库,命名为DomainLogic,然后添加一个名为ProductService的类:
public class ProductService {
private readonly CommerceObjectContext objectContext;
public ProductService()
{
this.objectContext = new CommerceObjectContext();
}
public IEnumerable<Product> GetFeaturedProducts(
bool isCustomerPreferred)
{
var discount = isCustomerPreferred ? .95m : 1;
var products = (from p in this.objectContext
.Products
where p.IsFeatured
select p).AsEnumerable();
return from p in products
select new Product
{
ProductId = p.ProductId,
Name = p.Name,
Description = p.Description,
IsFeatured = p.IsFeatured,
UnitPrice = p.UnitPrice * discount
};
}
}
现在我们的业务逻辑层已经实现了。
1.1.3 表现层
现在实现表现层逻辑,这里使用ASP.NET MVC,在Index 页面的Controller中,获取商品列表然后将数据返回给View。
public ViewResult Index()
{
bool isPreferredCustomer =
this.User.IsInRole("PreferredCustomer");
var service = new ProductService();
var products =
service.GetFeaturedProducts(isPreferredCustomer);
this.ViewData["Products"] = products;
return this.View();
}
然后在View中将Controller中返回的数据展现出来:
<h2>Featured Products</h2>
<div>
<% var products =
(IEnumerable<Product>)this.ViewData["Products"];
foreach (var product in products)
{ %>
<div>
<%= this.Html.Encode(product.Name) %>
(<%= this.Html.Encode(product.UnitPrice.ToString("C")) %>)
</div>
<% } %>
</div>
1.2 分析
现在,按照三层“架构”我们的代码写好了,并且也达到了要求。整个项目的结构如下图:
Solution layout
这应该是我们通常经常写的所谓的三层架构。在Visual Studio中,三层之间的依赖可以通过项目引用表现出来。
1.2.1 依赖关系图
现在我们来分析一下,这三层之间的依赖关系,很明显,上面的实现中,DomianLogic需要依赖SqlDataAccess,因为DomainLogic中用到了Product这一实体,而这个实体是定义在DataAccess这一层的。WebUI这一层需要依赖DomainLogic,因为ProductService在这一层,同时,还需要依赖DataAccess,因为在UI中也用到了Product实体,现在整个系统的依赖关系是这样的:
Dependency graph in three-tier architecture
1.2.2 耦合性分析
使用三层结构的主要目的是分离关注点,当然还有一个原因是可测试性。我们应该将领域模型从数据访问层和表现层中分离出来,这样这两个层的变化才不会污染领域模型。在大的系统中,这点很重要,这样才能将系统中的不同部分隔离开来。
现在来看之前的实现中,有没有模块性,有没有那个模块可以隔离出来呢。现在添加几个新的case来看,系统是否能够响应这些需求:
添加新的用户界面
除了WebForm用户之外,可能还需要一个WinForm的界面,现在我们能否复用领域层和数据访问层呢?从依赖图中可以看到,没有任何一个模块会依赖表现层,因此很容易实现这一点变化。我们只需要创建一个WPF的富客户端就可以。现在整个系统的依赖图如下:
WPF client
更换新的数据源
可能过了一段时间,需要把整个系统部署到云上,要使用其他的数据存储技术,比如Azure Table Storage Service。现在,整个访问数据的协议发生了变化,访问Azure Table Storage Service的方式是Http协议,而之前的大多数.NET 访问数据的方式都是基于ADO.NET 的方式。并且数据源的保存方式也发生了改变,之前是关系型数据库,现在变成了key-value型数据库。
Azure datatable
由上面的依赖关系图可以看出,所有的层都依赖了数据访问层,如果修改数据访问层,则领域逻辑层,和表现层都需要进行相应的修改。
1.2.3 问题
除了上面的各层之间耦合下过强之外,代码中还有其他问题。
领域模型似乎都写到了数据访问层中。所以领域模型看起来依赖了数据访问层。在数据访问层中定义了名为Product的类,这种类应该是属于领域模型层的。
表现层中掺入了决定某个用户是否是会员的逻辑。这种业务逻辑应该是 业务逻辑层中应该处理的,所以也应该放到领域模型层
ProductService因为依赖了数据访问层,所以也会依赖在web.config 中配置的数据库连接字符串等信息。这使得,整个业务逻辑层也需要依赖这些配置才能正常运行。
在View中,包含了太多了函数性功能。他执行了强制类型转换,字符串格式化等操作,这些功能应该是在界面显示得模型中完成。
上面可能是我们大多数写代码时候的实现, UI界面层去依赖了数据访问层,有时候偷懒就直接引用了这一层,因为实体定义在里面了。业务逻辑层也是依赖数据访问层,直接在业务逻辑里面使用了数据访问层里面的实体。这样使得整个系统紧耦合,并且可测试性差。那现在我们看看,如何修改这样一个系统,使之达到松散耦合,从而提高可测试性呢?
2 较好的实现
依赖注入能够较好的解决上面出现的问题,现在可以使用这一思想来重新实现前面的系统。之所以重新实现是因为,前面的实现在一开始的似乎就没有考虑到扩展性和松耦合,使用重构的方式很难达到理想的效果。对于小的系统来说可能还可以,但是对于一个大型的系统,应该是比较困难的。
在写代码的时候,要管理好依赖性,在前面的实现这种,代码直接控制了依赖性:当ProductService需要一个ObjectContext类的似乎,直接new了一个,当HomeController需要一个ProductService的时候,直接new了一个,这样看起来很酷很方便,实际上使得整个系统具有很大的局限性,变得紧耦合。new 操作实际上就引入了依赖, 控制反转这种思想就是要使的我们比较好的管理依赖。
2.1 松耦合的代码
2.1.1 表现层
首先从表现层来分析,表现层主要是用来对数据进行展现,不应该包含过多的逻辑。在Index的View页面中,代码希望可以写成这样
<h2>
Featured Products</h2>
<div>
<% foreach (var product in this.Model.Products)
{ %>
<div>
<%= this.Html.Encode(product.SummaryText) %></div>
<% } %>
</div>
可以看出,跟之前的表现层代码相比,要整洁很多。很明显是不需要进行类型转换,要实现这样的目的,只需要让Index.aspx这个视图继承自 System.Web.Mvc.ViewPage<FeaturedProductsViewModel> 即可,当我们在从Controller创建View的时候,可以进行选择,然后会自动生成。整个用于展示的信息放在了SummaryText字段中。
这里就引入了一个视图模型(View-Specific Models),他封装了视图的行为,这些模型只是简单的POCOs对象(Plain Old CLR Objects)。FeatureProductsViewModel中包含了一个List列表,每个元素是一个ProductViewModel类,其中定义了一些简单的用于数据展示的字段。
FeatureProductsViewModel
现在在Controller中,我们只需要给View返回FeatureProductsViewModel对象即可。比如:
public ViewResult Index()
{
var vm = new FeaturedProductsViewModel();
return View(vm);
}
现在返回的是空列表,具体的填充方式在领域模型中,我们接着看领域模型层。
2.1.2 领域逻辑层
新建一个类库,这里面包含POCOs和一些抽象类型。POCOs用来对领域建模,抽象类型提供抽象作为到达领域模型的入口。依赖注入的原则是面向接口而不是具体的类编程,使得我们可以替换具体实现。
现在我们需要为表现层提供数据。因此用户界面层需要引用领域模型层。对数据访问层的简单抽象可以采用Patterns of Enterprise Application Architecture一书中讲到的Repository模式。因此定义一个ProductRepository抽象类,注意是抽象类,在领域模型库中。它定义了一个获取所有特价商品的抽象方法:
public abstract class ProductRepository
{
public abstract IEnumerable<Product> GetFeaturedProducts();
}
这个方法的Product类中只定义了商品的基本信息比如名称和单价。整个关系图如下:
Domain model
现在来看表现层,HomeController中的Index方法应该要使用ProductService实例类来获取商品列表,执行价格打折,并且把Product类似转化为ProductViewModel实例,并将该实例加入到FeaturesProductsViewModel中。因为ProductService有一个带有类型为ProductReposity抽象类的构造函数,所以这里可以通过构造函数注入实现了ProductReposity抽象类的实例。这里和之前的最大区别是,我们没有使用new关键字来立即new一个对象,而是通过构造函数的方式传入具体的实现。
现在来看表现层代码:
public partial class HomeController : Controller
{
private readonly ProductRepository repository;
public HomeController(ProductRepository repository)
{
if (repository == null)
{
throw new ArgumentNullException("repository");
}
this.repository = repository;
}
public ViewResult Index()
{
var productService = new ProductService(this.repository);
var vm = new FeaturedProductsViewModel();
var products = productService.GetFeaturedProducts(this.User);
foreach (var product in products)
{
var productVM = new ProductViewModel(product);
vm.Products.Add(productVM);
}
return View(vm);
}
}
在HomeController的构造函数中,传入了实现了ProductRepository抽象类的一个实例,然后将该实例保存在定义的私有的只读的ProductRepository类型的repository对象中,这就是典型的通过构造函数注入。在Index方法中,获取数据的ProductService类中的主要功能,实际上是通过传入的repository类来代理完成的。
ProductService类是一个纯粹的领域对象,实现如下:
public class ProductService
{
private readonly ProductRepository repository;
public ProductService(ProductRepository repository)
{
if (repository == null)
{
throw new ArgumentNullException("repository");
}
this.repository = repository;
}
public IEnumerable<DiscountedProduct> GetFeaturedProducts(IPrincipal user)
{
if (user == null)
{
throw new ArgumentNullException("user");
}
return from p in
this.repository.GetFeaturedProducts()
select p.ApplyDiscountFor(user);
}
}
可以看到ProductService也是通过构造函数注入的方式,保存了实现了ProductReposity抽象类的实例,然后借助该实例中的GetFeatureProducts方法,获取原始列表数据,然后进行打折处理,进而实现了自己的GetFeaturedProducts方法。在该GetFeaturedProducts方法中,跟之前不同的地方在于,现在的参数是IPrincipal,而不是之前的bool型,因为判断用户的状况,这是一个业务逻辑,不应该在表现层处理。IPrincipal是BCL中的类型,所以不存在额外的依赖。我们应该基于接口编程IPrincipal是应用程序用户的一种标准方式。
这里将IPrincipal作为参数传递给某个方法,然后再里面调用实现的方式是依赖注入中的方法注入的手段。和构造函数注入一样,同样是将内部实现代理给了传入的依赖对象。
现在我们只剩下两块地方没有处理了:
没有ProductRepository的具体实现,这个很容易实现,后面放到数据访问层里面去处理,我们只需要创建一个具体的实现了ProductRepository的数据访问类即可。
默认上,ASP.NET MVC 希望Controller对象有自己的默认构造函数,因为我们在HomeController中添加了新的构造函数来注入依赖,所以MVC框架不知道如何解决创建实例,因为有依赖。这个问题可以通过开发一个IControllerFactory来解决,该对象可以创建一个具体的ProductRepositry实例,然后传给HomeController这里不多讲。
现在我们的领域逻辑层已经写好了。在该层,我们只操作领域模型对象,以及.NET BCL 中的基本对象。模型使用POCOs来表示,命名为Product。领域模型层必须能够和外界进行交流(database),所以需要一个抽象类(Repository)来时完成这一功能,并且在必要的时候,可以替换具体实现。
2.1.3 数据访问层
现在我们可以使用LINQ to Entity来实现具体的数据访问层逻辑了。因为要实现领域模型的ProductRepository抽象类,所以需要引入领域模型层。注意,这里的依赖变成了数据访问层依赖领域模型层。跟之前的恰好相反,代码实现如下:
public class SqlProductRepository : Domain.ProductRepository
{
private readonly CommerceObjectContext context;
public SqlProductRepository(string connString)
{
this.context =
new CommerceObjectContext(connString);
}
public override IEnumerable<Domain.Product> GetFeaturedProducts()
{
var products = (from p in this.context.Products
where p.IsFeatured
select p).AsEnumerable();
return from p in products
select p.ToDomainProduct();
}
}
在这里需要注意的是,在领域模型层中,我们定义了一个名为Product的领域模型,然后再数据访问层中Entity Framework帮我们也生成了一个名为Product的数据访问层实体,他是和db中的Product表一一对应的。所以我们在方法返回的时候,需要把类型从db中的Product转换为领域模型中的POCOs Product对象。
two product class in the system
Domain Model中的Product是一个POCOs类型的对象,他仅仅包含领域模型中需要用到的一些基本字段,DataAccess中的Product对象是映射到DB中的实体,它包含数据库中Product表定义的所有字段,在数据表现层中我们 定义了一个ProductViewModel数据展现的Model。
这两个对象之间的转换很简单:
public class Product
{
public Domain.Product ToDomainProduct()
{
Domain.Product p = new Domain.Product();
p.Name = this.Name;
p.UnitPrice = this.UnitPrice;
return p;
}
}
2.2 分析
2.2.1 依赖关系图
现在,整个系统的依赖关系图如下:
Dependency graph in DDD
表现层和数据访问层都依赖领域模型层,这样,在前面的case中,如果我们新添加一个UI界面;更换一种数据源的存储和获取方式,只需要修改对应层的代码即可,领域模型层保持了稳定。
2.2.2 时序图
整个系统的时序图如下:
Sequence Diagram
系统启动的时候,在Global.asax中创建了一个自定义了Controller工厂类,应用程序将其保存在本地便两种,当页面请求进来的时候,程序出发该工厂类的CreateController方法,并查找web.config中的数据库连接字符串,将其传递给新的SqlProductRepository实例,然后将SqlProductRepository实例注入到HomeControll中,并返回。
然后应用调用HomeController的实例方法Index来创建新的ProductService类,并通过构造函数传入SqlProductRepository。ProductService的GetFeaturedProducts 方法代理给SqlProductRepository实例去实现。
最后,返回填充好了FeaturedProductViewModel的ViewResult对象给页面,然后MVC进行合适的展现。
2.2.3 新的结构
在1.1的实现中,采用了三层架构,在改进后的实现中,在UI层和领域模型层中加入了一个表现模型(presentation model)层。如下图:
presentation model layer
将Controllers和ViewModel从表现层移到了表现模型层,仅仅将视图(.aspx和.ascx文件)和聚合根对象(Composition Root)保留在了表现层中。之所以这样处理,是可以使得尽可能的使得表现层能够可配置而其他部分尽可能的可以保持不变。
3. 结语
一不小心我们就编写出了紧耦合的代码,有时候以为分层了就可以解决这一问题,但是大多数的时候,都没有正确的实现分层。之所以容易写出紧耦合的代码有一个原因是因为编程语言或者开发环境允许我们只要需要一个新的实例对象,就可以使用new关键字来实例化一个。如果我们需要添加依赖,Visual Studio有些时候可以自动帮我们添加引用。这使得我们很容易就犯错,使用new关键字,就可能会引入以来;添加引用就会产生依赖。
减少new引入的依赖及紧耦合最好的方式是使用构造函数注入依赖这种设计模式:即如果我们需要一个依赖的实例,通过构造函数注入。在第二个部分的实现演示了如何针对抽象而不是具体编程。
构造函数注入是反转控制的一个例子,因为我们反转了对依赖的控制。不是使用new关键字创建一个实例,而是将这种行为委托给了第三方实现。
希望本文能够给大家了解如何真正实现三层架构,编写松散耦合,可维护,可测试性的代码提供一些帮助。
========
理解依赖注入(IOC)和学习Unity
http://www.cnblogs.com/zhangchenliang/archive/2013/01/08/2850970.htmlIOC:英文全称:Inversion of Control,中文名称:控制反转,它还有个名字叫依赖注入(Dependency Injection)。
作用:将各层的对象以松耦合的方式组织在一起,解耦,各层对象的调用完全面向接口。当系统重构的时候,代码的改写量将大大减少。
理解依赖注入:
当一个类的实例需要另一个类的实例协助时,在传统的程序设计过程中,通常有调用者来创建被调用者的实例。然而采用依赖注入的方式,创建被调用者的工作不再由调用者来完成,因此叫控制反转,创建被调用者的实例的工作由IOC容器来完成,然后注入调用者,因此也称为依赖注入。
举个有意思的例子(来源于互联网)
假如我们要设计一个Girl和一个Boy类,其中Girl有Kiss方法,即Girl想要Kiss一个Boy,首先问题是Girl如何认识Boy?
在我们中国常见的MM认识GG的方式有以下几种:
A 青梅竹马 B 亲友介绍 C 父母包办
哪一种是最好的?
1.青梅竹马:很久很久以前,有个有钱的地主家的一闺女叫Lily,她老爸把她许配给县太爷的儿子Jimmy,属于指腹为婚,Lily非常喜欢kiss,但是只能kiss Jimmy
public class Lily{
public Jimmy jimmy;
public Girl()
{
jimmy=new Jimmy();
}
public void Kiss()
{
jimmy.Kiss();
}
}
public class Jimmy
{
public void Kiss()
{
Console.WriteLine("kissing");
}
}
这样导致Lily对Jimmy的依赖性非常强,紧耦合。
2.亲友介绍:经常Kiss同一个人令Lily有些厌恶了,她想尝试新人,于是与Jimmy分手了,通过亲朋好友(中间人)来介绍
public class Lily{
public Boy boy;
public Girl()
{
boy=BoyFactory.createBoy();
}
public void Kiss()
{
boy.Kiss();
}
}
亲友介绍,固然是好。如果不满意,尽管另外换一个好了。但是,亲友BoyFactory经常是以Singleton的形式出现,不然就是,存在于Globals,无处不在,无处不能。实在是太繁琐了一点,不够灵活。我为什么一定要这个亲友掺和进来呢?为什么一定要付给她介绍费呢?万一最好的朋友爱上了我的男朋友呢?
3.父母包办:一切交给父母,自己不用非吹灰之力,Lily在家只Kiss
public class Lily{
public Boy boy;
public Girl(Boy boy)
{
this.boy=boy;
}
public void Kiss()
{
this.boy.Kiss();
}
}
Well,这是对Girl最好的方法,只要想办法贿赂了Girl的父母,并把Boy交给他。那么我们就可以轻松的和Girl来Kiss了。看来几千年传统的父母之命还真是有用哦。至少Boy和Girl不用自己瞎忙乎了。这就是IOC,将对象的创建和获取提取到外部。由外部容器提供需要的组件。
在设计模式中我们应该还知道依赖倒转原则,应是面向接口编程而不是面向功能实现,好处是:多实现可以任意切换,我们的Boy应该是实现Kissable接口。这样一旦Girl不想kiss可恶的Boy的话,还可以kiss可爱的kitten和慈祥的grandmother
好在.net中微软有一个轻量级的IoC框架Unity,支持构造器注入,属性注入,方法注入如下图所示
具体使用方法如下图所示
using System;
using Microsoft.Practices.Unity;
namespace ConsoleApplication9
{
class Program
{
static void Main(string[] args)
{
//创建容器
IUnityContainer container=new UnityContainer();
//注册映射
container.RegisterType<IKiss, Boy>();
//得到Boy的实例
var boy = container.Resolve<IKiss>();
Lily lily = new Lily(boy);
lily.kiss();
}
}
public interface IKiss
{
void kiss();
}
public class Lily:IKiss
{
public IKiss boy;
public Lily(IKiss boy)
{
this.boy=boy;
}
public void kiss()
{
boy.kiss();
Console.WriteLine("lily kissing");
}
}
public class Boy : IKiss
{
public void kiss()
{
Console.WriteLine("boy kissing");
}
}
}
如果采用配置文件注册的话
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,Microsoft.Practices.Unity.Configuration"/>
</configSections>
<unity>
<containers>
<container name="defaultContainer">
<register type="命名空间.接口类型1,命名空间" mapTo="命名空间.实现类型1,命名空间" />
<register type="命名空间.接口类型2,命名空间" mapTo="命名空间.实现类型2,命名空间" />
</container>
</containers>
</unity>
</configuration>
配置的后台代码:
UnityConfigurationSection configuration = ConfigurationManager.GetSection(UnityConfigurationSection.SectionName)
as UnityConfigurationSection;
configuration.Configure(container, "defaultContainer");
可以通过方法ResolveAll来得到所有注册对象的实例:
var Instances = container.Resolve<IKiss>();
Martin Fowler在那篇著名的文章《Inversion of Control Containers and the Dependency Injection pattern》中将具体依赖注入划分为三种形式,即构造器注入、属性(设置)注入和接口注入,习惯将其划分为一种(类型)匹配和三种注入:
类型匹配(Type Matching):虽然我们通过接口(或者抽象类)来进行服务调用,但是服务本身还是实现在某个具体的服务类型中,这就需要某个类型注册机制来解决服务接口和服务类型之间的匹配关系;
构造器注入(Constructor Injection):IoC容器会智能地选择选择和调用适合的构造函数以创建依赖的对象。如果被选择的构造函数具有相应的参数,IoC容器在调用构造函数之前解析注册的依赖关系并自行获得相应参数对象;
属性注入(Property Injection):如果需要使用到被依赖对象的某个属性,在被依赖对象被创建之后,IoC容器会自动初始化该属性;
方法注入(Method Injection):如果被依赖对象需要调用某个方法进行相应的初始化,在该对象创建之后,IoC容器会自动调用该方法。
我们创建一个控制台程序,定义如下几个接口(IA、IB、IC和ID)和它们各自的实现类(A、B、C、D)。在类型A中定义了3个属性B、C和D,其类型分别为接口IB、IC和ID。其中属性B在构在函数中被初始化,以为着它会以构造器注入的方式被初始化;属性C上应用了DependencyAttribute特性,意味着这是一个需要以属性注入方式被初始化的依赖属性;属性D则通过方法Initialize初始化,该方法上应用了特性InjectionMethodAttribute,意味着这是一个注入方法在A对象被IoC容器创建的时候会被自动调用。
public interface IA { }
public interface IB { }
public interface IC { }
public interface ID { }
public class A : IA
{
public IB B { get; set; }
[Dependency]
public IC C { get; set; }
public ID D { get; set; }
public A(IB b)
{
this.B = b;
}
[InjectionMethod]
public void Initalize(ID d)
{
this.D = d;
}
}
public class B : IB { }
public class C : IC { }
public class D : ID { }
然后我们为该应用添加一个配置文件,并定义如下一段关于Unity的配置。这段配置定义了一个名称为defaultContainer的Unity容器,并在其中完成了上面定义的接口和对应实现类之间映射的类型匹配。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,Microsoft.Practices.Unity.Configuration"/>
</configSections>
<unity>
<containers>
<container name="defaultContainer">
<register type="UnityDemo.IA,UnityDemo" mapTo="UnityDemo.A, UnityDemo"/>
<register type="UnityDemo.IB,UnityDemo" mapTo="UnityDemo.B, UnityDemo"/>
<register type="UnityDemo.IC,UnityDemo" mapTo="UnityDemo.C, UnityDemo"/>
<register type="UnityDemo.ID,UnityDemo" mapTo="UnityDemo.D, UnityDemo"/>
</container>
</containers>
</unity>
</configuration>
最后在Main方法中创建一个代表IoC容器的UnityContainer对象,并加载配置信息对其进行初始化。然后调用它的泛型的Resolve方法创建一个实现了泛型接口IA的对象。最后将返回对象转变成类型A,并检验其B、C和D属性是否是空
class Program
{
static void Main(string[] args)
{
UnityContainer container = new UnityContainer();
UnityConfigurationSection configuration = ConfigurationManager.GetSection(UnityConfigurationSection.SectionName) as UnityConfigurationSection;
configuration.Configure(container, "defaultContainer");
A a = container.Resolve<IA>() as A;
if (null!=a)
{
Console.WriteLine("a.B==null?{0}",a.B==null?"Yes":"No");
Console.WriteLine("a.C==null?{0}", a.C == null ? "Yes" : "No");
Console.WriteLine("a.D==null?{0}", a.D == null ? "Yes" : "No");
}
}
}
从如下给出的执行结果我们可以得到这样的结论:通过Resolve<IA>方法返回的是一个类型为A的对象,该对象的三个属性被进行了有效的初始化。这个简单的程序分别体现了接口注入(通过相应的接口根据配置解析出相应的实现类型)、构造器注入(属性B)、属性注入(属性C)和方法注入(属性D)
a.B == null ? No
a.C == null ? No
a.D == null ? No
========
spring四种依赖注入方式
http://kb.cnblogs.com/page/45266/4/平常的java开发中,程序员在某个类中需要依赖其它类的方法,则通常是new一个依赖类再调用类实例的方法,这种开发存在的问题是new的类实例不好统一管理,spring提出了依赖注入的思想,即依赖类不由程序员实例化,而是通过spring容器帮我们new指定实例并且将实例注入到需要该对象的类中。依赖注入的另一种说法是“控制反转”,通俗的理解是:平常我们new一个实例,这个实例的控制权是我们程序员,而控制反转是指new实例工作不由我们程序员来做而是交给spring容器来做。
spring有多种依赖注入的形式,下面仅介绍spring通过xml进行IOC配置的方式:
Set注入
这是最简单的注入方式,假设有一个SpringAction,类中需要实例化一个SpringDao对象,那么就可以定义一个private的SpringDao成员变量,然后创建SpringDao的set方法(这是ioc的注入入口):
Java代码 收藏代码
package com.bless.springdemo.action;
public class SpringAction {
//注入对象springDao
private SpringDao springDao;
//一定要写被注入对象的set方法
public void setSpringDao(SpringDao springDao) {
this.springDao = springDao;
}
public void ok(){
springDao.ok();
}
}
随后编写spring的xml文件,<bean>中的name属性是class属性的一个别名,class属性指类的全名,因为在SpringAction中有一个公共属性Springdao,所以要在<bean>标签中创建一个<property>标签指定SpringDao。<property>标签中的name就是SpringAction类中的SpringDao属性名,ref指下面<bean name="springDao"...>,这样其实是spring将SpringDaoImpl对象实例化并且调用SpringAction的setSpringDao方法将SpringDao注入:
Java代码 收藏代码
<!--配置bean,配置后该类由spring管理-->
<bean name="springAction" class="com.bless.springdemo.action.SpringAction">
<!--(1)依赖注入,配置当前类中相应的属性-->
<property name="springDao" ref="springDao"></property>
</bean>
<bean name="springDao" class="com.bless.springdemo.dao.impl.SpringDaoImpl"></bean>
构造器注入
这种方式的注入是指带有参数的构造函数注入,看下面的例子,我创建了两个成员变量SpringDao和User,但是并未设置对象的set方法,所以就不能支持第一种注入方式,这里的注入方式是在SpringAction的构造函数中注入,也就是说在创建SpringAction对象时要将SpringDao和User两个参数值传进来:
Java代码 收藏代码
public class SpringAction {
//注入对象springDao
private SpringDao springDao;
private User user;
public SpringAction(SpringDao springDao,User user){
this.springDao = springDao;
this.user = user;
System.out.println("构造方法调用springDao和user");
}
public void save(){
user.setName("卡卡");
springDao.save(user);
}
}
在XML文件中同样不用<property>的形式,而是使用<constructor-arg>标签,ref属性同样指向其它<bean>标签的name属性:
Xml代码 收藏代码
<!--配置bean,配置后该类由spring管理-->
<bean name="springAction" class="com.bless.springdemo.action.SpringAction">
<!--(2)创建构造器注入,如果主类有带参的构造方法则需添加此配置-->
<constructor-arg ref="springDao"></constructor-arg>
<constructor-arg ref="user"></constructor-arg>
</bean>
<bean name="springDao" class="com.bless.springdemo.dao.impl.SpringDaoImpl"></bean>
<bean name="user" class="com.bless.springdemo.vo.User"></bean>
解决构造方法参数的不确定性,你可能会遇到构造方法传入的两参数都是同类型的,为了分清哪个该赋对应值,则需要进行一些小处理:
下面是设置index,就是参数位置:
Xml代码 收藏代码
<bean name="springAction" class="com.bless.springdemo.action.SpringAction">
<constructor-arg index="0" ref="springDao"></constructor-arg>
<constructor-arg index="1" ref="user"></constructor-arg>
</bean>
另一种是设置参数类型:
Xml代码 收藏代码
<constructor-arg type="java.lang.String" ref=""/>
静态工厂的方法注入
静态工厂顾名思义,就是通过调用静态工厂的方法来获取自己需要的对象,为了让spring管理所有对象,我们不能直接通过"工程类.静态方法()"来获取对象,而是依然通过spring注入的形式获取:
Java代码 收藏代码
package com.bless.springdemo.factory;
import com.bless.springdemo.dao.FactoryDao;
import com.bless.springdemo.dao.impl.FactoryDaoImpl;
import com.bless.springdemo.dao.impl.StaticFacotryDaoImpl;
public class DaoFactory {
//静态工厂
public static final FactoryDao getStaticFactoryDaoImpl(){
return new StaticFacotryDaoImpl();
}
}
同样看关键类,这里我需要注入一个FactoryDao对象,这里看起来跟第一种注入一模一样,但是看随后的xml会发现有很大差别:
Java代码 收藏代码
public class SpringAction {
//注入对象
private FactoryDao staticFactoryDao;
public void staticFactoryOk(){
staticFactoryDao.saveFactory();
}
//注入对象的set方法
public void setStaticFactoryDao(FactoryDao staticFactoryDao) {
this.staticFactoryDao = staticFactoryDao;
}
}
Spring的IOC配置文件,注意看<bean name="staticFactoryDao">指向的class并不是FactoryDao的实现类,而是指向静态工厂DaoFactory,并且配置 factory-method="getStaticFactoryDaoImpl"指定调用哪个工厂方法:
Xml代码 收藏代码
<!--配置bean,配置后该类由spring管理-->
<bean name="springAction" class="com.bless.springdemo.action.SpringAction" >
<!--(3)使用静态工厂的方法注入对象,对应下面的配置文件(3)-->
<property name="staticFactoryDao" ref="staticFactoryDao"></property>
</property>
</bean>
<!--(3)此处获取对象的方式是从工厂类中获取静态方法-->
<bean name="staticFactoryDao" class="com.bless.springdemo.factory.DaoFactory" factory-method="getStaticFactoryDaoImpl"></bean>
实例工厂的方法注入
实例工厂的意思是获取对象实例的方法不是静态的,所以你需要首先new工厂类,再调用普通的实例方法:
Java代码 收藏代码
public class DaoFactory {
//实例工厂
public FactoryDao getFactoryDaoImpl(){
return new FactoryDaoImpl();
}
}
那么下面这个类没什么说的,跟前面也很相似,但是我们需要通过实例工厂类创建FactoryDao对象:
Java代码 收藏代码
public class SpringAction {
//注入对象
private FactoryDao factoryDao;
public void factoryOk(){
factoryDao.saveFactory();
}
public void setFactoryDao(FactoryDao factoryDao) {
this.factoryDao = factoryDao;
}
}
最后看spring配置文件:
Xml代码 收藏代码
<!--配置bean,配置后该类由spring管理-->
<bean name="springAction" class="com.bless.springdemo.action.SpringAction">
<!--(4)使用实例工厂的方法注入对象,对应下面的配置文件(4)-->
<property name="factoryDao" ref="factoryDao"></property>
</bean>
<!--(4)此处获取对象的方式是从工厂类中获取实例方法-->
<bean name="daoFactory" class="com.bless.springdemo.factory.DaoFactory"></bean>
<bean name="factoryDao" factory-bean="daoFactory" factory-method="getFactoryDaoImpl"></bean>
总结
Spring IOC注入方式用得最多的是(1)(2)种,多谢多练就会非常熟练。
另外注意:通过Spring创建的对象默认是单例的,如果需要创建多实例对象可以在<bean>标签后面添加一个属性:
Java代码 收藏代码
<bean name="..." class="..." scope="prototype">
========
深度理解依赖注入
http://kb.cnblogs.com/page/45266/4/摘要:提到依赖注入,大家都会想到老马那篇经典的文章。其实,本文就是相当于对那篇文章的解读。所以,如果您对原文已经有了非常深刻的理解,完全不需要再看此文;但是,如果您和笔者一样,以前曾经看过,似乎看懂了,但似乎又没抓到什么要领,不妨看看笔者这个解读,也许对您理解原文有一定帮助。
[1] 依赖在哪里
[2] DI的实现方式
[3] Setter Injection
[4] 除了DI,还有Service Locator
1.依赖在哪里
老马举了一个小例子,是开发一个电影列举器(MovieList),这个电影列举器需要使用一个电影查找器(MovieFinder)提供的服务,伪码如下:
1/*服务的接口*/
2public interface MovieFinder {
3 ArrayList findAll();
4}
5
6/*服务的消费者*/
7class MovieLister
8{
9 public Movie[] moviesDirectedBy(String arg) {
10 List allMovies = finder.findAll();
11 for (Iterator it = allMovies.iterator(); it.hasNext();) {
12 Movie movie = (Movie) it.next();
13 if (!movie.getDirector().equals(arg)) it.remove();
14 }
15 return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
16 }
17
18 /*消费者内部包含一个将指向具体服务类型的实体对象*/
19 private MovieFinder finder;
20 /*消费者需要在某一个时刻去实例化具体的服务。这是我们要解耦的关键所在,
21 *因为这样的处理方式造成了服务消费者和服务提供者的强耦合关系(这种耦合是在编译期就确定下来的)。
22 **/
23 public MovieLister() {
24 finder = new ColonDelimitedMovieFinder("movies1.txt");
25 }
26}
从上面代码的注释中可以看到,MovieLister和ColonDelimitedMovieFinder(这可以使任意一个实现了MovieFinder接口的类型)之间存在强耦合关系,如下图所示:
图1
这使得MovieList很难作为一个成熟的组件去发布,因为在不同的应用环境中(包括同一套软件系统被不同用户使用的时候),它所要依赖的电影查找器可能是千差万别的。所以,为了能实现真正的基于组件的开发,必须有一种机制能同时满足下面两个要求:
(1)解除MovieList对具体MoveFinder类型的强依赖(编译期依赖)。
(2)在运行的时候为MovieList提供正确的MovieFinder类型的实例。
换句话说,就是在运行的时候才产生MovieList和MovieFinder之间的依赖关系(把这种依赖关系在一个合适的时候“注入”运行时),这恐怕就是Dependency Injection这个术语的由来。再换句话说,我们提到过解除强依赖,这并不是说MovieList和MovieFinder之间的依赖关系不存在了,事实上MovieList无论如何也需要某类MovieFinder提供的服务,我们只是把这种依赖的建立时间推后了,从编译器推迟到运行时了。
依赖关系在OO程序中是广泛存在的,只要A类型中用到了B类型实例,A就依赖于B。前面笔者谈到的内容是把概念抽象到了服务使用者和服务提供者的角度,这也符合现在SOA的设计思路。从另一种抽象方式上来看,可以把MovieList看成我们要构建的主系统,而MovieFinder是系统中的plugin,主系统并不强依赖于任何一个插件,但一旦插件被加载,主系统就应该可以准确调用适当插件的功能。
其实不管是面向服务的编程模式,还是基于插件的框架式编程,为了实现松耦合(服务调用者和提供者之间的or框架和插件之间的),都需要在必要的位置实现面向接口编程,在此基础之上,还应该有一种方便的机制实现具体类型之间的运行时绑定,这就是DI所要解决的问题。
2.DI的实现方式
和上面的图1对应的是,如果我们的系统实现了依赖注入,组件间的依赖关系就变成了图2:
图2
说白了,就是要提供一个容器,由容器来完成(1)具体ServiceProvider的创建(2)ServiceUser和ServiceProvider的运行时绑定。下面我们就依次来看一下三种典型的依赖注入方式的实现。特别要说明的是,要理解依赖注入的机制,关键是理解容器的实现方式。本文后面给出的容器参考实现,均为黄忠成老师的代码,笔者仅在其中加上了一些关键注释而已。
2.1 Constructor Injection(构造器注入)
我们可以看到,在整个依赖注入的数据结构中,涉及到的重要的类型就是ServiceUser, ServiceProvider和Assembler三者,而这里所说的构造器,指的是ServiceUser的构造器。也就是说,在构造ServiceUser实例的时候,才把真正的ServiceProvider传给他:
1class MovieLister
2{
3 //其他内容,省略
4
5 public MovieLister(MovieFinder finder)
6 {
7 this.finder = finder;
8 }
9}
接下来我们看看Assembler应该如何构建:
1private MutablePicoContainer configureContainer() {
2 MutablePicoContainer pico = new DefaultPicoContainer();
3
4 //下面就是把ServiceProvider和ServiceUser都放入容器的过程,以后就由容器来提供ServiceUser的已完成依赖注入实例,
5 //其中用到的实例参数和类型参数一般是从配置档中读取的,这里是个简单的写法。
6 //所有的依赖注入方法都会有类似的容器初始化过程,本文在后面的小节中就不再重复这一段代码了。
7 Parameter[] finderParams = {new ConstantParameter("movies1.txt")};
8 pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);
9 pico.registerComponentImplementation(MovieLister.class);
10 //至此,容器里面装入了两个类型,其中没给出构造参数的那一个(MovieLister)将依靠其在构造器中定义的传入参数类型,在容器中
11 //进行查找,找到一个类型匹配项即可进行构造初始化。
12 return pico;
13}
需要在强调一下的是,依赖并未消失,只是延后到了容器被构建的时刻。所以正如图2中您已经看到的,容器本身(更准确的说,是一个容器运行实例的构建过程)对ServiceUser和ServiceProvoder都是存在依赖关系的。所以,在这样的体系结构里,ServiceUser、ServiceProvider和容器都是稳定的,互相之间也没有任何依赖关系;所有的依赖关系、所有的变化都被封装进了容器实例的创建过程里,符合我们对服务应用的理解。而且,在实际开发中我们一般会采用配置文件来辅助容器实例的创建,将这种变化性排斥到编译期之外。
即使还没给出后面的代码,你也一定猜得到,这个container类一定有一个GetInstance(Type t)这样的方法,这个方法会为我们返回一个已经注入完毕的MovieLister。 一个简单的应用如下:
1public void testWithPico()
2{
3 MutablePicoContainer pico = configureContainer();
4 MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class);
5 Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
6 assertEquals("Once Upon a Time in the West", movies[0].getTitle());
7}
上面最关键的就是对pico.getComponentInstance的调用。Assembler会在这个时候调用MovieLister的构造器,构造器的参数就是当时通过pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams)设置进去的实际的ServiceProvider--ColonMovieFinder。下面请看这个容器的参考代码:
2.2 Setter Injection(设值注入)
这种注入方式和构造注入实在很类似,唯一的区别就是前者在构造函数的调用过程中进行注入,而它是通过给属性赋值来进行注入。无怪乎PicoContainer和Spring都是同时支持这两种注入方式。Spring对通过XML进行配置有比较好的支持,也使得Spring中更常使用设值注入的方式:
1<beans>
2 <bean id="MovieLister" class="spring.MovieLister">
3 <property name="finder">
4 <ref local="MovieFinder"/>
5 property>
6 bean>
7 <bean id="MovieFinder" class="spring.ColonMovieFinder">
8 <property name="filename">
9 <value>movies1.txtvalue>
10 property>
11 bean>
12beans>
下面也给出支持设值注入的容器参考实现,大家可以和构造器注入的容器对照起来看,里面的差别很小,主要的差别就在于,在获取对象实例(GetInstance)的时候,前者是通过反射得到待创建类型的构造器信息,然后根据构造器传入参数的类型在容器中进行查找,并构造出合适的实例;而后者是通过反射得到待创建类型的所有属性,然后根据属性的类型在容器中查找相应类型的实例。
设值注入的容器实现伪码
2.3 Interface Injection (接口注入)
这是笔者认为最不够优雅的一种依赖注入方式。要实现接口注入,首先ServiceProvider要给出一个接口定义:
1public interface InjectFinder {
2 void injectFinder(MovieFinder finder);
3}
接下来,ServiceUser必须实现这个接口:
1class MovieLister: InjectFinder
2{
3 public void injectFinder(MovieFinder finder) {
4 this.finder = finder;
5 }
6}
容器所要做的,就是根据接口定义调用其中的inject方法完成注入过程,这里就不在赘述了,总的原理和上面两种依赖注入模式没有太多区别。
2.4 除了DI,还有Service Locator
上面提到的依赖注入只是消除ServiceUser和ServiceProvider之间的依赖关系的一种方法,还有另一种方法:服务定位器(Service Locator)。也就是说,由ServiceLocator来专门负责提供具体的ServiceProvider。当然,这样的话ServiceUser不仅要依赖于服务的接口,还依赖于ServiceContract。仍然是最早提到过的电影列举器的例子,如果使用Service Locator来解除依赖的话,整个依赖关系应当如下图所示:
图3
用起来也很简单,在一个适当的位置(比如在一组相关服务即将被调用之前)对ServiceLocator进行初始化,用到的时候就直接用ServiceLocator返回ServiceProvider实例:
1//服务定位器的初始化
2ServiceLocator locator = new ServiceLocator();
3locator.loadService("MovieFinder", new ColonMovieFinder("movies1.txt"));
4ServiceLocator.load(locator);
5//服务定义器的使用
6//其实这个使用方式体现了服务定位器和依赖注入模式的最大差别:ServiceUser需要显示的调用ServiceLocator,从而获取自己需要的服务对象;
7//而依赖注入则是隐式的由容器完成了这一切。
8MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder");
9
正因为上面提到过的ServiceUser对ServiceLocator的依赖性,从提高模块的独立性(比如说,你可能把你构造的ServiceUser或者ServiceProvider给第三方使用)上来说,依赖注入可能更好一些,这恐怕也是为什么大多数的IOC框架都选用了DI的原因。ServiceLocator最大的优点可能在于实现起来非常简单,如果您开发的应用没有复杂到需要采用一个IOC框架的程度,也许您可以试着采用它。
3.广义的服务
文中很多地方提到服务使用者(ServiceUser)和服务提供者(ServiceProvider)的概念,这里的“服务”是一种非常广义的概念,在语法层面就是指最普通的依赖关系(类型A中有一个B类型的变量,则A依赖于B)。如果您把服务理解为WCF或者Web Service中的那种服务概念,您会发现上面所说的所有技术手段都是没有意义的。以WCF而论,其客户端和服务器端本就是依赖于Contract的松耦合关系,其实这也从另一个角度说明了SOA应用的优势所在。
========