IoC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

其实IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。

IoC很好的体现了面向对象设计法则之一:由IoC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。

第一个例子

一个叫IGame的游戏公司,正在开发一款ARPG游戏(动作&角色扮演类游戏,如魔兽世界、梦幻西游这一类的游戏)。一般这类游戏都有一个基本的功能,就是打怪(玩家攻击怪物,借此获得经验、虚拟货币和虚拟装备),并且根据玩家角色所装备的武器不同,攻击效果也不同.打怪功能中的某一个功能:

1、角色可向怪物实施攻击,一次攻击后,怪物掉部分HP,HP掉完后,怪物死亡。

2、角色可装配不同武器,有木剑、铁剑、魔剑。

3、木剑每次攻击,怪物掉20PH,铁剑掉50HP,魔剑掉100PH。

一般实现

HP当然是怪物的一个属性成员,而武器是角色的一个属性成员,类型可以使字符串,用于描述目前角色所装备的武器。角色类有一个攻击方法,以被攻击怪物为参数,当实施一次攻击时,攻击方法被调用,而这个方法首先判断当前角色装备了什么武器,然后据此对被攻击怪物的HP进行操作,以产生不同效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IGameLi
{
/// <summary>
/// 怪物
/// </summary>
internal sealed class Monster
{
/// <summary>
/// 怪物的名字
/// </summary>
public String Name { get; set; }

/// <summary>
/// 怪物的生命值
/// </summary>
public Int32 HP { get; set; }

public Monster(String name,Int32 hp)
{
this.Name = name;
this.HP = hp;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IGameLi
{
/// <summary>
/// 角色
/// </summary>
internal sealed class Role
{
private Random _random = new Random();

/// <summary>
/// 表示角色目前所持武器的字符串
/// </summary>
public String WeaponTag { get; set; }

/// <summary>
/// 攻击怪物
/// </summary>
/// <param name="monster">被攻击的怪物</param>
public void Attack(Monster monster)
{
if (monster.HP <= 0)
{
Console.WriteLine("此怪物已死");
return;
}

if ("WoodSword" == this.WeaponTag)
{
monster.HP -= 20;
if (monster.HP <= 0)
{
Console.WriteLine("攻击成功!怪物" + monster.Name + "已死亡");
}
else
{
Console.WriteLine("攻击成功!怪物" + monster.Name + "损失20HP");
}
}
else if ("IronSword" == this.WeaponTag)
{
monster.HP -= 50;
if (monster.HP <= 0)
{
Console.WriteLine("攻击成功!怪物" + monster.Name + "已死亡");
}
else
{
Console.WriteLine("攻击成功!怪物" + monster.Name + "损失50HP");
}
}
else if ("MagicSword" == this.WeaponTag)
{
Int32 loss = (_random.NextDouble() < 0.5) ? 100 : 200;
monster.HP -= loss;
if (200 == loss)
{
Console.WriteLine("出现暴击!!!");
}

if (monster.HP <= 0)
{
Console.WriteLine("攻击成功!怪物" + monster.Name + "已死亡");
}
else
{
Console.WriteLine("攻击成功!怪物" + monster.Name + "损失" + loss + "HP");
}
}
else
{
Console.WriteLine("角色手里没有武器,无法攻击!");
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IGameLi
{
class Program
{
static void Main(string[] args)
{
//生成怪物
Monster monster1 = new Monster("小怪A", 50);
Monster monster2 = new Monster("小怪B", 50);
Monster monster3 = new Monster("关主", 200);
Monster monster4 = new Monster("最终Boss", 1000);

//生成角色
Role role = new Role();

//木剑攻击
role.WeaponTag = "WoodSword";
role.Attack(monster1);

//铁剑攻击
role.WeaponTag = "IronSword";
role.Attack(monster2);
role.Attack(monster3);

//魔剑攻击
role.WeaponTag = "MagicSword";
role.Attack(monster3);
role.Attack(monster4);
role.Attack(monster4);
role.Attack(monster4);
role.Attack(monster4);
role.Attack(monster4);

Console.ReadLine();
}
}
}

存在问题:

Role类的Attack方法很长,并且方法中有一个冗长的if…else结构,且每个分支的代码的业务逻辑很相似,只是很少的地方不同

违反了OCP原则。在这个设计中,如果以后我们增加一个新的武器,如倚天剑,每次攻击损失500HP,那么,我们就要打开Role,修改Attack方法。而我们的代码应该是对修改关闭的,当有新武器加入的时候,应该使用扩展完成,避免修改已有代码。

一般来说,当一个方法里面出现冗长的if…else或switch…case结构,且每个分支代码业务相似时,往往预示这里应该引入多态性来解决问题。而这里,如果把不同武器攻击看成一个策略,那么引入策略模式(Strategy Pattern)是明智的选择。

最后说一个小的问题,被攻击后,减HP、死亡判断等都是怪物的职责,这里放在Role中有些不当

Tip:OCP原则,即开放关闭原则,指设计应该对扩展开放,对修改关闭。

Tip:策略模式,英文名Strategy Pattern,指定义算法族,分别封装起来,让他们之间可以相互替换,此模式使得算法的变化独立于客户。

第二种实现

1
2
3
4
5
6
7
8
9
10
11
12
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IGameLiAdv
{
internal interface IAttackStrategy
{
void AttackTarget(Monster monster);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IGameLiAdv
{
internal sealed class WoodSword : IAttackStrategy
{
public void AttackTarget(Monster monster)
{
monster.Notify(20);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IGameLiAdv
{
internal sealed class IronSword : IAttackStrategy
{
public void AttackTarget(Monster monster)
{
monster.Notify(50);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IGameLiAdv
{
internal sealed class MagicSword : IAttackStrategy
{
private Random _random = new Random();

public void AttackTarget(Monster monster)
{
Int32 loss = (_random.NextDouble() < 0.5) ? 100 : 200;
if (200 == loss)
{
Console.WriteLine("出现暴击!!!");
}
monster.Notify(loss);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IGameLiAdv
{
/// <summary>
/// 怪物
/// </summary>
internal sealed class Monster
{
/// <summary>
/// 怪物的名字
/// </summary>
public String Name { get; set; }

/// <summary>
/// 怪物的生命值
/// </summary>
private Int32 HP { get; set; }

public Monster(String name,Int32 hp)
{
this.Name = name;
this.HP = hp;
}

/// <summary>
/// 怪物被攻击时,被调用的方法,用来处理被攻击后的状态更改
/// </summary>
/// <param name="loss">此次攻击损失的HP</param>
public void Notify(Int32 loss)
{
if (this.HP <= 0)
{
Console.WriteLine("此怪物已死");
return;
}

this.HP -= loss;
if (this.HP <= 0)
{
Console.WriteLine("怪物" + this.Name + "被打死");
}
else
{
Console.WriteLine("怪物" + this.Name + "损失" + loss + "HP");
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IGameLiAdv
{
/// <summary>
/// 角色
/// </summary>
internal sealed class Role
{
/// <summary>
/// 表示角色目前所持武器
/// </summary>
public IAttackStrategy Weapon { get; set; }

/// <summary>
/// 攻击怪物
/// </summary>
/// <param name="monster">被攻击的怪物</param>
public void Attack(Monster monster)
{
this.Weapon.AttackTarget(monster);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IGameLiAdv
{
class Program
{
static void Main(string[] args)
{
//生成怪物
Monster monster1 = new Monster("小怪A", 50);
Monster monster2 = new Monster("小怪B", 50);
Monster monster3 = new Monster("关主", 200);
Monster monster4 = new Monster("最终Boss", 1000);

//生成角色
Role role = new Role();

//木剑攻击
role.Weapon = new WoodSword();
role.Attack(monster1);

//铁剑攻击
role.Weapon = new IronSword();
role.Attack(monster2);
role.Attack(monster3);

//魔剑攻击
role.Weapon = new MagicSword();
role.Attack(monster3);
role.Attack(monster4);
role.Attack(monster4);
role.Attack(monster4);
role.Attack(monster4);
role.Attack(monster4);

Console.ReadLine();
}
}
}

优点:

第一,虽然类的数量增加了,但是每个类中方法的代码都非常短,没有了以前Attack方法那种很长的方法,也没有了冗长的if…else,代码结构变得很清晰。

第二,类的职责更明确了。在第一个设计中,Role不但负责攻击,还负责给怪物减少HP和判断怪物是否已死。这明显不应该是Role的职责,改进后的代码将这两个职责移入Monster内,使得职责明确,提高了类的内聚性。

第三,引入Strategy模式后,不但消除了重复性代码,更重要的是,使得设计符合了OCP。如果以后要加一个新武器,只要新建一个类,实现IAttackStrategy接口,当角色需要装备这个新武器时,客户代码只要实例化一个新武器类,并赋给Role的Weapon成员就可以了,已有的Role和Monster代码都不用改动。这样就实现了对扩展开发,对修改关闭。

初窥依赖注入

上面例子的第二种实现中,Role不依赖具体武器,而仅仅依赖一个IAttackStrategy接口,接口是不能实例化的,虽然Role的Weapon成员类型定义为IAttackStrategy,但最终还是会被赋予一个实现了IAttackStrategy接口的具体武器,并且随着程序进展,一个角色会装备不同的武器,从而产生不同的效用。赋予武器的职责,在Demo中是放在了测试代码里。

这里,测试代码实例化一个具体的武器,并赋给Role的Weapon成员的过程,就是依赖注入!这里要清楚,依赖注入其实是一个过程的称谓!

依赖注入产生的背景:

随着面向对象分析与设计的发展,一个良好的设计,核心原则之一就是将变化隔离,使得变化部分发生变化时,不变部分不受影响(这也是OCP的目的)。为了做到这一点,要利用面向对象中的多态性,使用多态性后,客户类不再直接依赖服务类,而是依赖于一个抽象的接口,这样,客户类就不能在内部直接实例化具体的服务类。但是,客户类在运作中又客观需要具体的服务类提供服务,因为接口是不能实例化去提供服务的。就产生了“客户类不准实例化具体服务类”和“客户类需要具体服务类”这样一对矛盾。为了解决这个矛盾,开发人员提出了一种模式:客户类(如上例中的Role)定义一个注入点(Public成员Weapon),用于服务类(实现IAttackStrategy的具体类,如WoodSword、IronSword和MagicSword,也包括以后加进来的所有实现IAttackStrategy的新类)的注入,而客户类的客户类(Program,即测试代码)负责根据情况,实例化服务类,注入到客户类中,从而解决了这个矛盾。

依赖注入的正式定义:

依赖注入(Dependency Injection),是这样一个过程:由于某客户类只依赖于服务类的一个接口,而不依赖于具体服务类,所以客户类只定义一个注入点。在程序运行过程中,客户类不直接实例化具体服务类实例,而是客户类的运行上下文环境或专门组件负责实例化服务类,然后将其注入到客户类中,保证客户类的正常运行。

依赖注入的类别

依赖注入有很多种方法,上面看到的例子中,只是其中的一种,下面从另一个例子了解不同的依赖注入类型。

第二个例子

《墨攻》这部电影讲述了战国时期墨家人革离帮助梁国反抗赵国侵略的个人英雄主义故事,恢宏壮阔,浑雄凝重的历史场面相当震撼。其中有一个场景:当刘德华所饰的墨者革离到达梁国都城下,城上梁国守军问:“来者何人?”,刘德华回答:“墨者革离!”,对这段“城门问对”的场景进行编剧并借由这个例子来理解IoC的内涵。

剧本与演员直接耦合

MoAttack代表《墨攻》的剧本,CityGetAsk()代表“城门问对”这段剧情,LiuDeHua是具体饰演者刘德华

1
2
3
4
5
6
7
public class LiuDeHua
{
public void ResponseAsk(string res)
{
Console.WriteLine(res);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MoAttack
{

public MoAttack() { }

public void CityGateAsk()
{

LiuDeHua ldh = new LiuDeHua();//演员直接侵入剧本

ldh.ResponseAsk("墨者革离!");

}

}

我们会发现,作为具体饰演者的刘德华直接侵入到剧本中,使剧本和演员直接耦合在一起

引入剧本角色

一个明智的编剧在剧情创作时应围绕故事的角色进行,而不应考虑角色的具体饰演者,这样才可能在剧本投拍时自由地选择任何适合的演员,而非绑定在刘德华一人身上。通过以上的分析,我们知道需要为该剧本主人公革离定义一个接口,以角色进行剧情安排,饰演者实现角色的接口.

添加革离角色接口

1
2
3
4
public interface IGeLi
{
void ResponseAsk(string res);
}

饰演者实现接口

1
2
3
4
5
6
7
public class LiuDeHua:IGeLi
{
public void ResponseAsk(string res)
{
Console.WriteLine(res);
}
}

剧本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MoAttack
{

public MoAttack() { }

public void CityGateAsk()
{

IGeLi ldh = new LiuDeHua();

ldh.ResponseAsk("墨者革离!");

}

}

剧本和饰演者解耦

我们希望剧本和演员无关,可是即使加入了剧本角色,我们看到MoAttack同时依赖于IGeLi接口和LiuDeHua类,并没有达到我们所期望的剧本仅依赖于角色的目的。可是角色最终又必须通过具体的演员才能完成拍摄,如何将让LiuDeHua和剧本无关而又能完成IGeLi的具体动作呢?当然是在影片投拍时,导演将LiuDeHua安排在GeLi的角色上,通过导演之手将剧本、角色、饰演者装配起来。

构造函数注入

新的MoAttack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MoAttack
{
private IGeLi geli;

public MoAttack(IGeLi geli)//注入革离的具体扮演者
{
this.geli = geli;
}

public void CityGateAsk()
{

this.geli.ResponseAsk("墨者革离!");

}

}

MoAttack的构造函数不关心具体是谁扮演革离这个角色,只要传入的扮演者按剧本要求完成角色功能即可

角色的具体扮演者由导演来安排:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Director
{
public void direct()
{

IGeLi geli = new LiuDeHua(); //指定角色的扮演者

MoAttack moAttack = new MoAttack(geli); //注入具体扮演者到剧本中

moAttack.CityGateAsk();

}
}

属性注入

有时,导演会发现,虽然革离是影片《墨攻》的第一主人公,但并非每场戏都需要革离的出现,通过构造函数方式注入显得很不妥当,在这种情况下,可以使用属性注入进行改造。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MoAttack
{
private IGeLi geli;

public IGeLi GeLi //属性注入方法
{
set { this.geli=value;}
}

public void CityGateAsk()
{

this.geli.ResponseAsk("墨者革离!");

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Director
{
public void direct()
{

IGeLi geli = new LiuDeHua(); //指定角色的扮演者

MoAttack moAttack = new MoAttack();

moAttack.GeLi = geli;//调用属性注入

moAttack.CityGateAsk();

}
}

MoAttack在geli 字段提供一个属性,以便让导演在拍需要革离的戏时才将geli的具体扮演者注入,而不需要刘德华从头到尾跟着墨攻剧组跑

和通过构造函数注入革离扮演者不同,在实例化MoAttack时,并未指定任何扮演者,而是在实例化MoAttack后,调用其属性注入扮演者。按照类似的方式,我们还可以为剧本中其他如巷淹中,梁王等角色分别提供注入的属性,导演即可以根据所拍剧段的不同,注入所需要的角色了。

接口注入

将客户类所有注入的方法抽取到一个接口中,客户类通过实现这一接口提供注入的方法。为了采取接口注入的方式,需要声明一个额外的接口:

1
2
3
4
public interface IActorArrangable
{
void InjectGeli(IGeLi geli);
}

MoAttack实现这个接口并实现接口中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MoAttack : IActorArrangable
{
private IGeLi geli;

public void CityGateAsk()
{

this.geli.ResponseAsk("墨者革离!");

}

public void InjectGeli(IGeLi geli)
{
this.geli = geli;
}
}

Director通过IActorArrangable接口的injectGeli()方法完成扮演者的注入工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Director
{
public void direct()
{

IGeLi geli = new LiuDeHua(); //指定角色的扮演者

MoAttack moAttack = new MoAttack();

moAttack.InjectGeli(geli);

moAttack.CityGateAsk();

}
}

由于通过接口注入需要额外声明一个接口,增加了类的数目,而且它的效果和属性注入并无本质区别,因此我们不提倡这种方式

通过容器完成依赖关系的建立

虽然MoAttack和LiuDeHua实现了解耦,无需关注实现类的实例化工作,但这些工作在代码中依然存在,只是转移到Director中而已,导致导演的权力非常大,潜规则不断滋生。假设某一制片人想改变这一局面,在相中某个剧本后,通过一个“海选”或者第三公正中介来选择导演、演员,让他们各司其职,那剧本、导演、演员就都实现解耦了。

所谓媒体“海选”和中介机构在程序领域即是一个第三方容器,它帮助我们完成类的初始化和装配工作,让我们从这些底层的实现类实例化,依赖关系的装配中脱离出来,专注于更有意思的业务代码的编写工作,那确实是挺惬意的事情。

Spring.Net就是这样一个容器,它通过配置文件描述类之间的依赖关系,下面是Spring.Net配置文件的对以上实例进行配置的样式代码:

1
2
3
4
5
6
7
8
9
10
11
<objects>

<object id="geli" type="RuoXie.IOCTest.LiuDeHua"></object>

<object id="moAttack" type="RuoXie.IOCTest.MoAttack">

<property name="geli"><ref="geli"/></property>

</object>

</objects>

参考文档:

依赖注入那些事儿

墨攻IOC