Ioc(Inverse of control)已经是叫嚷了很久的技术了,一直没有机会细看,最近因为看源代码的关系,研究了一点,拿出来分享一下。
当前网络上有很多Ioc的框架,比如说微软的企业库就使用Ioc技术重写了,还有Prism模式也用到了Ioc。我看的函数库是Autofac,但是理念跟其他的函数库大同小异,实际上,为了方便程序员在不同的Ioc框架上移植程序,各个框架的编写者开会定义了一个大家都支持的接口集:Common Service Locator。
什么是Ioc
Ioc简言之,就是将类似下面创建对象的代码—我们称之为情况1:
var checker
=
new
MemoChecker(memos,
new
PrintingNotifier(Console.Out));
转换成下面这样—称之为情况2:
var checker
=
container.Resolve
<
MemoCheck
>
();
而container.Resolve<MemoCheck>这一行代码在创建MemoCheck这个类型的实例时,又可以通过下面的代码创建MemoCheck构造函数所需要的两个参数:
new
MemoChecker(container.Resolve
<
IQueryable
<
Memo
>>
(),
container.Resolve
<
IMemoDueNotifier
>
())
情况2相对情况1的好处在于,在情况1 的代码里,程序员需要显式指定构建MemoChecker实例所要求的参数类型的实例。也就是说,MemoChecker在构造一个实例时,你需要显式传入第二个参数的具体实例(PrintingNotifier)。这样就导致一个问题,如果在后期程序发布以后,需要更换MemoCheck的第二个参数,那就只有修改程序代码一条路可走了。
针对于情况1的这个问题,那肯定有人会说,那就把MemoChecker构造函数的第二个参数定义成一个接口,然后在创建MemoChecker实例的时候,读一个配置文件,找到实现这个接口的具体类型,通过反射等机制创建对象传给MemoChecker的构造函数。这样就可以通过修改配置文件的方式,通过添加实现接口的插件,动态地修改程序的行为—这正是情况2所要做的,也就是Ioc和依赖注入(Dependence Injection)要解决的一个通用问题。
关于Ioc和依赖注入,网上已经有很多文章讲解这个概念了,有兴趣的朋友可以看看这篇文章,里面介绍的很详细:
http://martinfowler.com/articles/injection.html
使用Autofac实现依赖注入
我先以CodeProject的一个示例代码为例,讲解一下用Autofac实现依赖注入的基本步骤,下面是代码:
1
using
System;
2
using
System.Collections.Generic;
3
using
System.Linq;
4
using
System.IO;
5
using
Autofac;
6
7
namespace
Remember
8
{
9
interface
IMemoDueNotifier
10
{
11
void
MemoIsDue(Memo memo);
12
}
13
14
class
Memo
15
{
16
public
string
Title {
get
;
set
; }
17
public
DateTime DueAt {
get
;
set
; }
18
}
19
20
class
MemoChecker
21
{
22
readonly
IList
<
Memo
>
_memos;
23
readonly
IMemoDueNotifier _notifier;
24
25
public
MemoChecker(IList
<
Memo
>
memos, IMemoDueNotifier notifier)
26
{
27
_memos
=
memos;
28
_notifier
=
notifier;
29
}
30
31
public
void
CheckNow()
32
{
33
var overdueMemos
=
_memos.Where(memo
=>
memo.DueAt
<
DateTime.Now);
34
35
foreach
(var memo
in
overdueMemos)
36
_notifier.MemoIsDue(memo);
37
}
38
}
39
40
class
PrintingNotifier : IMemoDueNotifier
41
{
42
readonly
TextWriter _writer;
43
44
public
PrintingNotifier(TextWriter writer)
45
{
46
_writer
=
writer;
47
}
48
49
public
void
MemoIsDue(Memo memo)
50
{
51
_writer.WriteLine(
"
Memo '{0}' is due!
"
, memo.Title);
52
}
53
}
54
55
class
Program
56
{
57
static
void
Main()
58
{
59
var memos
=
new
List
<
Memo
>
{
60
new
Memo { Title
=
"
Release Autofac 1.1
"
,
61
DueAt
=
new
DateTime(
2007
,
03
,
12
) },
62
new
Memo { Title
=
"
Update CodeProject Article
"
,
63
DueAt
=
DateTime.Now },
64
new
Memo { Title
=
"
Release Autofac 3
"
,
65
DueAt
=
new
DateTime(
2011
,
07
,
01
) }
66
};
67
68
var builder
=
new
ContainerBuilder();
69
builder.Register(c
=>
new
MemoChecker(
70
c.Resolve
<
IList
<
Memo
>>
(), c.Resolve
<
IMemoDueNotifier
>
()));
71
builder.RegisterType
<
PrintingNotifier
>
().As
<
IMemoDueNotifier
>
();
72
builder.RegisterInstance(memos).As
<
IList
<
Memo
>>
();
73
74
builder.RegisterInstance(Console.Out)
75
.As
<
TextWriter
>
()
76
.ExternallyOwned();
77
78
using
(var container
=
builder.Build())
79
{
80
container.Resolve
<
MemoChecker
>
().CheckNow();
81
}
82
83
Console.WriteLine(
"
Done! Press any key.
"
);
84
Console.ReadKey();
85
}
86
}
87
}
88
这个程序的作用是检查所有的记事项,提醒用户这些过期的记事项。这个程序里最主要的类是MemoChecker,MemoChecker需要两个对象才能构建一个实例—Memo和IMemoDueNotifier。而这两个类型的对象,是由autofac自行解析的,autofac知道如何找到一个接口是由哪个对象实现的—这个过程叫做Resolve。而接口和实现接口对象的对映关系是由程序员在配置文件app.config,或者自己在程序的入口处(例如Main函数)注册好的—这个过程叫Register。因为实现接口的某些对象,有可能它的构造函数也会接受其他接口,而实现这些接口的对象也需要解析。因此,Autofac将所有的接口,和实现接口的对象都放到一个容器里,这个容器自己解析实现接口的对象之间的依赖关系—也就是ContainerBuilder。ContainerBuilder在Build的过程中,通过多次调用Resolve解决容器内部的对象依赖关系。当依赖关系都解析完毕以后,以后要创建对象,不需要再用类似下面的代码显式创建了:
var builder
=
new
MemoChecker();
创建对象的工作,全部都交给Container解决,Container自己在内部找到构造对象时,Container创建调用构造函数要用到的参数的对象,解决对象之间的依赖关系,然后你只要用类似下面的代码就可以获取到你要的对象:
var builder
=
container.Resolve
<
MemoChecker
>
();
使用Autofac基于配置文件实现依赖注入
前面讲到的依赖注入,还是基于代码的,很多时候,使用Ioc和依赖注入技术,主要是为了支持插件技术。比如说,其他插件只要实现了定义的接口,那么,终端用户理论上可以只通过将实现插件的assembly拷贝到程序文件夹,并修改配置文件的形式来无缝集成新的插件。
那我们来看Autofac自带的例子—Calculator。这个程序有三个Assembly组成,Calculator是那个支持插件的程序;Calculator.Api包括了接口的定义,这样,Calculator和它的插件通过引用这个Assembly,就可以实现相互交互了;而Calculator.Operations就是最后实现接口的一些插件。
我们来看一看代码:
Calculator.Api定义了一个接口—这个接口将会被Calculator(支持插件的程序)和Calculator.Operations(插件)所使用:
1
using
System;
2
3
namespace
Calculator.Api
4
{
5
public
interface
IOperation
6
{
7
string
Operator
8
{
9
get
;
10
}
11
12
double
Apply(
double
lhs,
double
rhs);
13
}
14
}
15
而在Calculator这个Assembly里,定义了一个Calculator这个类,枚举所有实现了IOperation的插件—这个枚举过程由Autofac自动完成:
1
using
System;
2
using
System.Collections.Generic;
3
using
System.Linq;
4
using
System.Text;
5
using
Calculator.Api;
6
7
namespace
Calculator
8
{
9
class
Calculator
10
{
11
IDictionary
<
string
, IOperation
>
_operations
=
new
Dictionary
<
string
, IOperation
>
();
12
13
public
Calculator(IEnumerable
<
IOperation
>
operations)
14
{
15
if
(operations
==
null
)
16
throw
new
ArgumentNullException(
"
operations
"
);
17
18
foreach
(IOperation op
in
operations)
19
_operations.Add(op.Operator, op);
20
}
21
22
public
IEnumerable
<
string
>
AvailableOperators
23
{
24
get
25
{
26
return
_operations.Keys;
27
}
28
}
29
30
public
double
ApplyOperator(
string
op,
double
lhs,
double
rhs)
31
{
32
if
(op
==
null
)
33
throw
new
ArgumentNullException(
"
op
"
);
34
35
IOperation operation;
36
if
(
!
_operations.TryGetValue(op,
out
operation))
37
throw
new
ArgumentException(
"
Unsupported operation.
"
);
38
39
return
operation.Apply(lhs, rhs);
40
}
41
}
42
}
43
请注意Calculator的构造函数,这个构造函数接受一个IEnumerable<IOperation>类型的参数,这个参数是autofac通过读取配置文件自动构建好一个实例,下面就是app.config文件里的具体设置:
1
<?
xml version="1.0"
?>
2
<
configuration
>
3
<
configSections
>
4
<
section
name
="calculator"
type
="Autofac.Configuration.SectionHandler, Autofac.Configuration"
/>
5
</
configSections
>
6
7
<
calculator
defaultAssembly
="Calculator.Api"
>
8
<
components
>
9
<
component
type
="Calculator.Operations.Add, Calculator.Operations"
member-of
="operations"
/>
10
<
component
type
="Calculator.Operations.Multiply, Calculator.Operations"
member-of
="operations"
/>
11
12
<
component
type
="Calculator.Operations.Divide, Calculator.Operations"
member-of
="operations"
>
13
<
parameters
>
14
<
parameter
name
="places"
value
="4"
/>
15
</
parameters
>
16
</
component
>
17
18
</
components
>
19
</
calculator
>
20
21
</
configuration
>
22
在程序(Calculator)启动的时候,调用Autofac API里面的ContainerBuilder.RegisterModule来告诉Autofac读取配置文件里的接口与实现接口对象的映射关系。
1
namespace
Calculator
2
{
3
4
static
class
Program
5
{
6
[STAThread]
7
static
void
Main()
8
{
9
try
10
{
11
var builder
=
new
ContainerBuilder();
12
13
...
14
15
builder.RegisterModule(
new
ConfigurationSettingsReader(
"
calculator
"
));
16
17
...
18
}
19
catch
(Exception ex)
20
{
21
DisplayException(ex);
22
}
23
}
24
}
25
}
26