这三个工具,应该是每个MVC程序员的兵工厂中的一部分。DI容器,单元测试框架,mocking 工具。Ninject是我们偏爱的DI容器,它简单,高雅,并且容易使用。这里有很多复杂的替代品,但是我们喜欢Ninject最小配置的工作方式。如果你不喜欢Ninject,可以使用Unity,它是微软提供的替代品。
单元测试方面,我们使用VS2010内置的 NUnit,它是.Net 单元测试框架中最受欢迎的一个。
Mocking 工具套装,我们选择 Moq。如果你不喜欢它,可以使用Rhino Mocks这个不错的替代品。
1 使用 Ninject
在原书中第四章,The MVC Pattern 中,我们介绍过DI的思想,用来在我们的MVC程序中的组件解耦。为了做到这点,我们需要结合接口和DI。
1
class
Program
2
{
3
static
void
Main(
string
[] args)
4
{
5
ShoppingCart cart
=
new
ShoppingCart(
new
LinqValueCalculator());
6
Console.WriteLine(
"
Total:{0}
"
, cart.CalculateStockValue());
7
Console.ReadKey();
8
}
9
}
10
11
public
class
Product
12
{
13
private
string
name;
14
public
int
ProductID {
get
;
set
; }
15
public
string
Name
16
{
17
get
{
return
name; }
18
set
{ name
=
value; }
19
}
20
public
string
Description {
get
;
set
; }
21
public
decimal
Price {
get
;
set
; }
22
public
string
Category {
get
;
set
; }
23
}
24
25
public
interface
IValueCalculator
26
{
27
decimal
ValueProducts(
params
Product[] products);
28
}
29
30
public
class
LinqValueCalculator : IValueCalculator
31
{
32
public
decimal
ValueProducts(
params
Product[] products)
33
{
34
return
products.Sum(prod
=>
prod.Price);
35
}
36
}
37
38
public
class
ShoppingCart
39
{
40
private
IValueCalculator calculator;
41
42
public
ShoppingCart(IValueCalculator calcParam)
43
{
44
calculator
=
calcParam;
45
}
46
47
public
decimal
CalculateStockValue()
48
{
49
Product[] products
=
50
{
51
new
Product() { Name
=
"
Kayak
"
, Price
=
275M},
52
new
Product() { Name
=
"
Lifejacket
"
, Price
=
48.95M
},
53
new
Product() { Name
=
"
Soccer ball
"
, Price
=
19.50M
},
54
new
Product() { Name
=
"
Stadium
"
, Price
=
79500M}
55
};
56
decimal
totalValue
=
calculator.ValueProducts(products);
57
return
totalValue;
58
}
59
}
IValueCalculator 接口中定义了一个方法,它接受一个或多个Product对象,并返回累积的值。我们将接口部署在 LinqValueCalculator类上,它使用Linq延期方法Sum,灵巧地生成Product对象们的Price属性合计。然后,我们需要创建一个会使用到IVaueCalculator的类,即ShoppingCart,这个类是为DI设计的。Shoppingcart类的构造器需要一个实现了IValueCalculator接口的类,作为参数,为DI做准备。CalculateStockValue方法创建一个Product对象数组,然后调用IValueCalculator接口中的ValueProducts,来得到合计。
我们成功地将ShoppingCart类和LinqValueCalculator类解耦,这两个类都依赖IValueCalculator,但是ShppingCart与LinqValueCalculator没有直接关系。事实上,它甚至不知道LinqValueCalculator的存在。我们能改变LinqValueCalculator的实现,甚至用一个新的IValuecalculator的实现完全替代它。
Product类与这三个类有直接关系,我们不用担心这点。Product 是一个 domain model 类型的等价物,我们期望这样的类,强耦合,依赖我们的程序。如果我们不构建MVC程序,我们也许持不同的观点,并解耦Product。
我们的目标是能够创建一个ShoppingCart的实例,并注入一个IValueCalculator类的实现,作为构造函数的参数。这是我们偏爱的DI容器Ninject所扮演的角色。但是在我们展示Ninject之前,我们需要设置VS。
1.1 使用Ninject开始
要开始使用Ninject,我们需要创建一个Ninject kernel的实例,这个对象,我们会用来与Ninject交流。
1
IKernel ninjectKernel
=
new
StandardKernel();
一旦创建kernel,Ninject会完成两个阶段的工作。第一是绑定你想要使用你已经创建的接口关联的类型。在这种情况下,我们想要告诉Ninject,当它收到一个请求,请求IValueCalculator的实例时,它应该创建并返回一个LinqValueCalculator类的实例。我们用定义在IKernel接口中的Bind和To方法做这样的事情。
1
ninjectKernel.Bind
<
IValueCalculator
>
().To
<
LinqValueCalculator
<
();
这段声明将IValueCalculator接口绑定到LinqValueCalculator实例类上。我们指定我们想要注册的接口,将它作为Bind方法的一般类型参数,并传递我们想要的具体实例的类型,作为第二个参数。
第二阶段,是使用Ninject的Get方法,创建一个实施接口的对象,并将它传递给ShoppingCart类的构造器。
1
IValueCalculator calcImpl
=
ninjectKernel.Get
<
IValueCalculator
>
();
2
ShoppingCart cart
=
new
ShoppingCart(calcImpl);
3
Console.WriteLine(
"
Total:{0:c}
"
, cart.TotalPrice());
我们指定我们想要实例化的接口,作为Get方法的一般类型参数。Ninject浏览我们定义的绑定,看到我们将IValueCalculator绑定到LinqValueCalculator,然后为我们创建一个新的实例。我们然后将实例注入到ShoppingCart类的构造器,并调用TotalPrice方法,它会反过来调用接口中定义的方法。
1
ShoppingCart cart
=
new
ShoppingCart(
new
LinqValueCalculator());
可以简化为这样。
1.2 创建依赖链
当我们请求Ninject创建一个类型,它会检查类型之间的耦合。如果有附加选项,Ninject解决他们,并创建所有必须的类的实例。
1
public
interface
IDiscountHelper
2
{
3
decimal
ApplyDiscount(
decimal
totalParam);
4
}
5
6
public
class
DefaultDiscountHelper : IDiscountHelper
7
{
8
public
decimal
ApplyDiscount(
decimal
totalParam)
9
{
10
return
(totalParam
-
(10m
/
100m
*
totalParam));
11
}
12
}
IDiscounHelper定义了一个ApplyDiscount方法,它会应用一个decima值折扣。DefaultDiscounterHelper类实现这个接口。我们可以将IDiscountHelper借口添加为LinqValueCalculator的依赖。
1
public
class
LinqValueCalculator : IValueCalculator
2
{
3
private
IDiscountHelper discounter;
4
5
public
LinqValueCalculator(IDiscountHelper discountParam)
6
{
7
discounter
=
discountParam;
8
}
9
10
public
decimal
ValueProducts(
params
Product[] products)
11
{
12
return
discounter.ApplyDiscount(products.Sum(prod
=>
prod.Price));
13
}
14
}
最新添加的构造器,需要传递一个IDiscountHelper接口的实现,它被用在ValueProducts方法,在处理累积Product对象的值时,应用打折。我们使用Ninject kernel将IDiscountHelper接口绑定到类的实现上。
1
ninjectKernel.Bind
<
IValueCalculator
>
().To
<
LinqValueCalculator
>
();
2
ninjectKernel.Bind
<
IDiscountHelper
>
().To
<
DefaultDiscountHelper
>
();
3
IValueCalculator calImpl
=
ninjectKernel.Get
<
IValueCalculator
>
();
4
ShoppingCart cart
=
new
ShoppingCart(calImpl);
我们不用不用改变任何代码来创建IValueCalculator的实现。当需要IValueCalculator时,Ninject知道我们想要LinqValueCalculator类被实例化。他已经检验过这个类,并发现它基于一个接口实现。Ninject创建一个DefaultDiscountHelper的实例,将它注入到LinqValueCalculator类的构造器,并将结果作为IValueCalculator返回。Ninject检查所有用这种方式实例化依赖的类,无论它的依赖链有多长或多复杂。
1.3 指定属性和参数的值
我们能配置Ninject创建的类,来提供当我们将接口绑定到它的实现上时的属性细节。我们修正了StandardDiscountHelper类,使它暴漏一个方便的属性,来指定折扣的程度。
1
public
class
DefaultDiscountHelper : IDiscountHelper
2
{
3
public
decimal
DiscountSize {
get
;
set
; }
4
5
public
decimal
ApplyDiscount(
decimal
totalParam)
6
{
7
return
(totalParam
-
(DiscountSize
/
100m
*
totalParam));
8
}
9
}
当我们使用Ninject将具体的类绑定到类型,我们可以使用WithPropertyValue方法,设置DefaultDiscountHelper类中DiscountSize属性的值。
1
ninjectKernel.Bind
<
IDiscountHelper
>
().To
<
DefaultDiscountHelper
>
().WithPropertyValue(
"
DiscountSize
"
,50M);
我们必须以字符串的形式提供属性的名字。我们不用改变任何其他的绑定,也不用改变Get方法的使用方式。属性的值,会随着DefaultDiscountHelper构建时设置。
如果你有多个值需要设置,可以链式调用WithPropertyValue方法。
也可以给构造函数传递参数
1
public
decimal
discountRate;
2
3
public
DefaultDiscountHelper(
decimal
discountParam)
4
{
5
discountRate
=
discountParam;
6
}
7
8
ninjectKernel.Bind
<
IDiscountHelper
>
().To
<
DefaultDiscountHelper
>
().WithConstructorArgument(
"
discountParam
"
, 50M);
1.4 使用自绑定
自绑定,是将Ninject完全整合进你代码的一个有用的特性,具体的类能从Ninject kernel请求。这看起来像是在做一件无聊的事情,但意味着我们不需要像以下代码那样,手工执行初始化DI。
1
IValueCalculator calcImpl
=
ninjectKernel.Get
<
IValueCalculator
>
();
2
ShoppingCart cart
=
new
ShoppingCart(calcImpl);
而是可以简单地请求一个ShoppingCart实例,让Ninject挑选出依赖于IValueCalculator类。
1
ShoppingCart cart
=
ninjectKernel.Get
<
ShoppingCart
>
();
如果我们花时间注册一个自绑定类型,我们能在接口上使用这些特性,像为构造器参数和属性指定值。要注册自绑定,偶们使用ToSelf方法
1
ninjectKernel.Bind
<
ShoppingCart
>
().ToSelf().WithParameter(
"
<parameterName>
"
,
<
paramvalue
>
);
ShoppingCart绑定自身,调用WithParameter方法,为虚构的属性提供值。你可以仅在具体的类上使用自绑定。
1.5 绑定到派生类型
尽管我们关注接口,我们也使用Ninject绑定具体的类。我们既能绑定具体的类自己,也能绑定到一个派生类型。
1
public
class
ShoppingCart
2
{
3
protected
IValueCalculator calculator;
4
protected
Product[] products;
5
6
public
ShoppingCart(IValueCalculator calcParam)
7
{
8
calculator
=
calcParam;
9
products
=
new
[]
10
{
11
new
Product{Name
=
"
Jiangyou
"
,Price
=
5M},
12
new
Product{Name
=
"
Zhijin
"
,Price
=
2.5M
}
13
};
14
}
15
16
public
virtual
decimal
CalculateStockValue()
17
{
18
decimal
total
=
calculator.ValueProducts(products);
19
return
total;
20
}
21
}
22
23
public
class
LimitShoppingCart : ShoppingCart
24
{
25
public
LimitShoppingCart(IValueCalculator calcParam):
base
(calcParam)
26
{
27
//
28
}
29
30
public
override
decimal
CalculateStockValue()
31
{
32
var filteredProducts
=
products.Where(e
=>
e.Price
<
ItemLimit);
33
return
calculator.ValueProducts(filteredProducts.ToArray());
34
}
35
public
decimal
ItemLimit {
get
;
set
; }
36
}
37
static
void
Main(
string
[] args)
38
{
39
IKernel ninjectKernel
=
new
StandardKernel();
40
ninjectKernel.Bind
<
IValueCalculator
>
().To
<
LinqValueCalculator
>
();
41
ninjectKernel.Bind
<
IDiscountHelper
>
().To
<
DefaultDiscountHelper
>
().WithConstructorArgument(
"
discountParam
"
, 50M);
42
ninjectKernel.Bind
<
ShoppingCart
>
().To
<
LimitShoppingCart
>
().WithPropertyValue(
"
ItemLimit
"
, 3M);
43
ShoppingCart cart
=
ninjectKernel.Get
<
ShoppingCart
>
();
44
Console.WriteLine(cart.CalculateStockValue());
45
Console.ReadKey();
46
}
我们能绑定父类,这样当我们从Ninject请求一个它的实例时,派生类的一个实例会被创建。这个技术用来绑定抽象类到它的具体实现时,工作的非常好。
1.6 使用条件绑定
使用Ninject,我们能绑定同一个接口的多个实现,或者同一个类的多个派生,使用指令,告诉它在不同的情况下应该使用哪个。下面我们创建一个IValueCalculator接口的新的实现:
1
public
class
IterativeValueCalculator : IValueCalculator
2
{
3
public
decimal
ValueProducts(
params
Product[] products)
4
{
5
decimal
total
=
0
;
6
foreach
(Product p
in
products)
7
{
8
total
+=
p.Price;
9
}
10
return
total;
11
}
12
}
13
14
ninjectKernel.Bind
<
IValueCalculator
>
().To
<
IterativeValueCalculator
>
().WhenInjectedInto
<
LimitShoppingCart
>
();
我们对IValueCalculator有一个原始的绑定,Ninject试图找到最匹配的绑定,如果条件不满足,它会使用默认绑定,到相同的类和接口。所以Ninject有一个退回的值。最有用的条件绑定方法:
Method |
Effect |
When(predicate) |
当条件中的Lambda表达式等于true时 |
WhenClassHas<T>() |
当类注入含有一个指定类型的属性 |
WhenInjectedInto<T>() |
当类被注入到类型T |
1.7 在mvc中使用Ninject
首先要创建一个派生自System.Web.Mvc.DefaultControllerFactory的类。DefaultControllerFactory类是MVc用来默认创建controller 类的使用。
这类创建了一个Ninject kernel,并使用它来为通过GetControllerInstance方法创建的,当它想要的一个controller对象时,被MVC框架调用的controller类,的请求服务。我们不需要使用Ninject明确地绑定controller类。我们依靠默认的自绑定特性,自从controller成为System.Web.Mvc.Controller派生的一个具体类。
AddBindings方法,允许我们为想要保持低耦合的套件和其他组件添加其他的Ninject绑定。我们也能使用这个方法,做诶一个绑定controller类的时机——需要附加构造器参数或属性值。
一旦我们创建了这个类,我们必须使用MVC框架注册它。
现在,MVC框架会使用我们的NinjectControllerFactory来获得controller类的实例,Ninject会自动处理DI到controller。