前面的话:提到依赖注入,大家都会想到老马那篇经典的文章 。其实, 本文就是相当于对那篇文章的解读。所以,如果您对原文已经有了非常深刻的理解,完全不需要再看此文;但是,如果您和笔者一样,以前曾经看过,似乎看懂了, 但似乎又没抓到什么要领,不妨看看笔者这个解读,也许对您理解原文有一定帮助。
1.依赖在哪里
老马举了一个小例子,是开发一个电影列举器(MovieList),这个电影列举器需要使用一个电影查找器(MovieFinder)提供的服务,伪码如 下:
2 public interface MovieFinder {
3 ArrayList findAll();
4 }
5
6 /* 服务的消费者 */
7 class 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传给他:
2 {
3 // 其他内容,省略
4
5 public MovieLister(MovieFinder finder)
6 {
7 this .finder = finder;
8 }
9 }
接下来我们看看Assembler应该如何构建:
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。 一个简单的应用如下:
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中更常使用 设值注入的方式:
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.txt </ value >
10 </ property >
11 </ bean >
12 </ beans >
下面也给出支持设值注入的容器参考实现,大家可以和构造器注入的容器对照起来看,里面的差别很小,主要的差别就在于,在获取对象实例 (GetInstance)的时候,前者是通过反射得到待创建类型的构造器信息,然后根据构造器传入参数的类型在容器中进行查找,并构造出合适的实例;而 后者是通过反射得到待创建类型的所有属性,然后根据属性的类型在容器中查找相应类型的实例。
2.3 Interface Injection (接口注入)
这是笔者认为最不够优雅的一种依赖注入方式。要实现接口注入,首先ServiceProvider要给出一个接口定义:
2 void injectFinder(MovieFinder finder);
3 }
接下来,ServiceUser必须实现这个接口:
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实例:
2 ServiceLocator locator = new ServiceLocator();
3 locator.loadService( " MovieFinder " , new ColonMovieFinder( " movies1.txt " ));
4 ServiceLocator.load(locator);
5 // 服务定义器的使用
6 // 其实这个使用方式体现了服务定位器和依赖注入模式的最大差别:ServiceUser需 要显示的调用ServiceLocator,从而获取自己需要的服务对象;
7 // 而依赖注入则是隐式的由容器完成了这一切。
8 MovieFinder 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应用的优势所在。
参考资料:
Object Builder Application Block
using
System;
using
System.Collections.Generic;
using
System.Text;
namespace
ConsoleApplication2
{
class Program
{
static void Main(string [] args)
{
Console .Write("Please Input Some Words:" );
string inputData = Console .ReadLine();
Console .WriteLine(inputData);
Console .Read();
}
}
}
|
using
System;
using
System.Collections.Generic;
using
System.ComponentModel;
using
System.Data;
using
System.Drawing;
using
System.Text;
using
System.Windows.Forms;
namespace
WindowsApplication10
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
MessageBox .Show(textBox1.Text);
}
}
}
|
using
System;
using
System.Collections.Generic;
using
System.Text;
namespace
DISimple
{
class Program
{
static void Main(string [] args)
{
InputAccept accept = new InputAccept (new PromptDataProcessor ());
accept.Execute();
Console .ReadLine();
}
}
public class InputAccept
{
private IDataProcessor _dataProcessor;
public void Execute()
{
Console .Write("Please Input some words:" );
string input = Console .ReadLine();
input = _dataProcessor.ProcessData(input);
Console .WriteLine(input);
}
public InputAccept(IDataProcessor dataProcessor)
{
_dataProcessor = dataProcessor;
}
}
public interface IDataProcessor
{
string ProcessData(string input);
}
public class DummyDataProcessor : IDataProcessor
{
#region
IDataProcessor Members
public string ProcessData(string input)
{
return input;
}
#endregion
}
public class PromptDataProcessor : IDataProcessor
{
#region
IDataProcessor Members
public string ProcessData(string input)
{
return "your input is: " + input;
}
#endregion
}
}
|
InputAccept accept = new InputAccept(new PromptDataProcessor());
|
//Factory
InputAccept
accept = new InputAccept(DataProcessorFactory.Create());
//Builder
InputAccept
accept = new InputAccept(DataProcessorBulder.Build());
|
//declare
public class DataProcessorFactory:IFactory ..........
//Builder
public class DataProcessorBuilder:IBuilder ...........
....................
//initialize
//Factory
GenericFactory.RegisterTypeFactory(typeof(IDataProcessor),typeof(DataProcessorFactory));
//Builder
GenericFactory.RegisterTypeBuilder(typeof(IDataProcessor),typeof(DataProcessorBuilder));
................
//Factory
InputAccept accept = new InputAccept(GenericFactory.Create(typeof(IDataProcessor));
//Builder
InputAccept accept = new InputAccept(GenericBuilder.Build(typeof(IDataProcessor));
|
PS:
這一小節進度超前許多,接下來將回歸
Dependency Injection
的三種模式,請注意!接下來幾小節的討論是依據三種模式的精神,所以例子以簡單易懂為主,不考慮本文 所提及的完整架構。
|
using
System;
using
System.Collections.Generic;
using
System.Text;
namespace
ConsoleApplication2
{
class Program
{
static void Main(string [] args)
{
InputAccept accept = new InputAccept ();
accept.Inject(new DummyDataProcessor ());
accept.Execute();
Console .Read();
}
}
public class InputAccept
{
private IDataProcessor _dataProcessor;
public void Inject(IDataProcessor dataProcessor)
{
_dataProcessor = dataProcessor;
}
public void Execute()
{
Console .Write("Please Input some words:" );
string input = Console .ReadLine();
input = _dataProcessor.ProcessData(input);
Console .WriteLine(input);
}
}
public interface IDataProcessor
{
string ProcessData(string input);
}
public class DummyDataProcessor : IDataProcessor
{
#region
IDataProcessor Members
public string ProcessData(string input)
{
return input;
}
#endregion
}
public class PromptDataProcessor : IDataProcessor
{
#region
IDataProcessor Members
public string ProcessData(string input)
{
return "your input is: " + input;
}
#endregion
}
}
|
public class InputAccept implements Serviceable {
private IDataProcessor m_dataProcessor;
public void service(ServiceManager sm) throws ServiceException {
m_dataProcessor = (IDataProcessor) sm.lookup("DataProcessor");
}
public void Execute() {
........
string input = m_dataProcessor.ProcessData(input);
........
}
}
|
PS:
在正確的Interface Injection定義中,組裝InputAccept與IDataProcessor的是容器,在本例中,我並未使用容器,而是提取其行為。
|
public
static class Container
{
private static Dictionary <Type , object > _stores = null ;
private static Dictionary <Type , object > Stores
{
get
{
if (_stores == null )
_stores = new Dictionary <Type , object >();
return _stores;
}
}
private static Dictionary <string ,object > CreateConstructorParameter(Type targetType)
{
Dictionary <string , object > paramArray = new Dictionary <string , object >();
ConstructorInfo [] cis = targetType.GetConstructors();
if (cis.Length > 1)
throw new Exception (
"target object has more then one constructor,container can't peek one for you."
);
foreach (ParameterInfo pi in cis[0].GetParameters())
{
if (Stores.ContainsKey(pi.ParameterType))
paramArray.Add(pi.Name, GetInstance(pi.ParameterType));
}
return paramArray;
}
public static object GetInstance(Type t)
{
if (Stores.ContainsKey(t))
{
ConstructorInfo [] cis = t.GetConstructors();
if (cis.Length != 0)
{
Dictionary <string , object > paramArray = CreateConstructorParameter(t);
List <object > cArray = new List <object >();
foreach (ParameterInfo pi in cis[0].GetParameters())
{
if (paramArray.ContainsKey(pi.Name))
cArray.Add(paramArray[pi.Name]);
else
cArray.Add(null );
}
return cis[0].Invoke(cArray.ToArray());
}
else if (Stores[t] != null )
return Stores[t];
else
return Activator .CreateInstance(t, false );
}
return Activator .CreateInstance(t, false );
}
public static void RegisterImplement(Type t, object impl)
{
if (Stores.ContainsKey(t))
Stores[t] = impl;
else
Stores.Add(t, impl);
}
public static void RegisterImplement(Type t)
{
if (!Stores.ContainsKey(t))
Stores.Add(t, null );
}
}
|
class
Program
{
static void Main(string [] args)
{
Container .RegisterImplement(typeof (InputAccept ));
Container .RegisterImplement(typeof (IDataProcessor ), new PromptDataProcessor ());
InputAccept accept = (InputAccept )Container .GetInstance(typeof (InputAccept ));
accept.Execute();
Console .Read();
}
}
public
class InputAccept
{
private IDataProcessor _dataProcessor;
public void Execute()
{
Console .Write("Please Input some words:" );
string input = Console .ReadLine();
input = _dataProcessor.ProcessData(input);
Console .WriteLine(input);
}
public InputAccept(IDataProcessor dataProcessor)
{
_dataProcessor = dataProcessor;
}
}
public
interface IDataProcessor
{
string ProcessData(string input);
}
public
class DummyDataProcessor : IDataProcessor
{
#region
IDataProcessor Members
public string ProcessData(string input)
{
return input;
}
#endregion
}
public
class PromptDataProcessor : IDataProcessor
{
#region
IDataProcessor Members
public string ProcessData(string input)
{
return "your input is: " + input;
}
#endregion
}
|
public
static class Container
{
private static Dictionary <Type , object > _stores = null ;
private static Dictionary <Type , object > Stores
{
get
{
if (_stores == null )
_stores = new Dictionary <Type , object >();
return _stores;
}
}
public static object GetInstance(Type t)
{
if (Stores.ContainsKey(t))
{
if (Stores[t] == null )
{
object target = Activator .CreateInstance(t, false );
foreach (PropertyDescriptor pd in TypeDescriptor .GetProperties(target))
{
if (Stores.ContainsKey(pd.PropertyType))
pd.SetValue(target, GetInstance(pd.PropertyType));
}
return target;
}
else
return Stores[t];
}
return Activator .CreateInstance(t, false );
}
public static void RegisterImplement(Type t, object impl)
{
if (Stores.ContainsKey(t))
Stores[t] = impl;
else
Stores.Add(t, impl);
}
public static void RegisterImplement(Type t)
{
if (!Stores.ContainsKey(t))
Stores.Add(t, null );
}
}
|
class
Program
{
static void Main(string [] args)
{
Container .RegisterImplement(typeof (InputAccept ));
Container .RegisterImplement(typeof (IDataProcessor ), new PromptDataProcessor ());
InputAccept accept = (InputAccept )Container .GetInstance(typeof (InputAccept ));
accept.Execute();
Console .Read();
}
}
public class InputAccept
{
private IDataProcessor _dataProcessor;
public IDataProcessor DataProcessor
{
get
{
return _dataProcessor;
}
set
{
_dataProcessor = value ;
}
}
public void Execute()
{
Console .Write("Please Input some words:" );
string input = Console .ReadLine();
input = _dataProcessor.ProcessData(input);
Console .WriteLine(input);
}
}
|