11、理解IoC模式
一、依赖
依赖就是有联系,有地方使用到它就是有依赖它,一个系统不可能完全避免依赖。如果你的一个类或者模块在项目中没有用到它,恭喜你,可以从项目中剔除它或者排除它了,因为没有一个地方会依赖它。下面看一个简单的示例:
///
/// 用户播放媒体文件
///
publicclassOperationMain
{
publicvoidPlayMedia()
{
MediaFile_mtype=newMediaFile();
Player_player=newPlayer();
_player.Play(_mtype);
}
}
///
/// 播放器
///
publicclassPlayer
{
publicvoidPlay(MediaFilefile)
{
Console.WriteLine(file.FilePath);
}
}
///
/// 媒体文件
///
publicclassMediaFile
{
publicstringFilePath{get;set;}
}
上面是一个用户用播放器播放文件简单示例,用户操作是OperationMain类中的PlayMedia方法,打开一个播放器,选择一个文件来播放。先看看他们之间的依赖关系,可以简单找到有3个依赖
Player依赖MediaFile
OperationMain依赖Player
OperationMain依赖MediaFile
二、2.依赖倒置
依赖倒置,字面意思就是依赖的东西,可以正着放,也可以倒着放,也就是说依赖的东西是可以变化。针对上述例子:播放器可以播放不同的媒体文件,而媒体文件也可以用不同的播放器进行播放。
耦合关系就是依赖关系,如果依赖关系相当繁杂,牵一发而动全身,很难维护;依赖关系越少,耦合关系就越低,系统就越稳定,所以我们要减少依赖。
幸亏Robert Martin大师提出了面向对象设计原则——依赖倒置原则:
A. 上层模块不应该依赖于下层模块,它们共同依赖于一个抽象。
B. 抽象不能依赖于具象,具象依赖于抽象。
理解:A.上层是使用者,下层是被使用者,这就导致的结果是上层依赖下层了,下层变动了,自然就会影响到上层了,导致系统不稳定,甚至是牵一发而动全身。那怎么减少依赖呢?就是上层和下层都去依赖另一个抽象,这个抽象比较稳定,整个就来说就比较稳定了。
B.面向对象编程时面向抽象或者面向借口编程,抽象一般比较稳定,实现抽象的具体肯定是要依赖抽象的,抽象不应该去依赖别的具体,应该依赖抽象。
上面播放器的示例中,我们已经找到依赖关系了,现在我们要按照依赖倒置原则,来进行优化。
根据原则如下改动:
Player依赖MediaFile,好办,让Player和MediaFile都去依赖一个抽象IMediaFile
OperationMain依赖Player,好办,让OperationMain和Player都依赖一个抽象IPlayer
OperationMain依赖MediaFile,好办,让OperationMain和MediaFile都依赖一个抽象IMediaFile
IPlayer不能依赖具体MediaFile,应该依赖于具体MediaFile的抽象IMediaFile
结构很简单,于是代码大致如下:
///
/// 用户播放媒体文件
///
publicclassOperationMain
{
publicvoidPlayMedia()
{
IMediaFile_mtype=newMediaFile();
IPlayer_player=newPlayer();
_player.Play(_mtype);
}
}
///
/// 播放器
///
publicinterfaceIPlayer
{
voidPlay(IMediaFilefile);
}
///
/// 默认播放器
///
publicclassPlayer:IPlayer
{
publicvoidPlay(IMediaFilefile)
{
Console.WriteLine(file.FilePath);
}
}
///
/// 媒体文件
///
publicinterfaceIMediaFile
{
stringFilePath{get;set;}
}
///
/// 默认媒体文件
///
publicclassMediaFile:IMediaFile
{
publicstringFilePath{get;set;}
}
上面代码进行了抽象,可以看到,目的是减少了依赖,但是看上去依赖关系增加了,如用户PlayMedia方法,依赖还增加了依赖接口和具体的实现,但是接口是稳定的,可以不考虑,具体的实现才是变动的,这个依赖还是要的,要播放文件,必定要用到具体的播放器和具体文件。
现实生活中,是具体的播放器和具体的媒体文件没有关系,你给它一个Mp3文件他可以播放,给它一个Mp4文件它也可以播放,你删掉你的媒体文件,播放器照样在,具体什么播放器,播放什么文件,控制权全部是我们用户自己。
上面的示例中基本实现了隔离,具体的播放器跟具体的媒体隔离了,具体的播放器只跟媒体接口和播放器接口有关。但是PlayMedia的方法里面的具体对象,写死了,控制权非常小,如果我想用百度影音播放呢,我想换一首音乐呢,只能重新改代码,那控制怎么进行转移呢?
我们可以通过反射来创建,把具体的文件名写在配置文件里,这时候客户端代码也不用变了,只需要改配置文件就好了,稳定性又有了提高,如下:
publicvoidPlayMedia()
{
IMediaFile_mtype=Assembly.Load(ConfigurationManager.AppSettings["AssemName"]).CreateInstance(ConfigurationManager.AppSettings["MediaName"]);
IPlayer_player=Assembly.Load(ConfigurationManager.AppSettings["AssemName"]).CreateInstance(ConfigurationManager.AppSettings["PlayerName"]);
_player.Play(_mtype);
}
依赖注入字面意思就是依赖注入的对象。
这就是IOC,将对象的创建和获取提取到外部。由外部容器提供需要的组件。
上面的示例中,哪些要依赖注入,依赖对象需要获得实例的地方,即 PlayMedia方法,需要IPlayer具体对象和IMediaFile的具体对象,找到了地方就从这里下手,为了灵活的控制这两个对象,必须是外面能够控制着两个对象的实例化,提供对外的操作是必要的,可以是属性,可以是方法,可以是构造函数,总之别的地方可以控制它,下面将会使用Unity来注入,使用的是构造函数注入,代码如下:
///
/// 用户播放媒体文件
///
publicclassOperationMain
{
IMediaFile_mtype;
IPlayer_player;
publicOperationMain(IPlayerplayer,IMediaFilemtype)
{
_player=player;
_mtype=mtype;
}
publicvoidPlayMedia()
{
_player.Play(_mtype);
}
}
///
/// 播放器
///
publicinterfaceIPlayer
{
voidPlay(IMediaFilefile);
}
///
/// 默认播放器
///
publicclassPlayer:IPlayer
{
publicvoidPlay(IMediaFilefile)
{
Console.WriteLine(file.FilePath);
}
}
///
/// 媒体文件
///
publicinterfaceIMediaFile
{
stringFilePath{get;set;}
}
///
/// 默认媒体文件
///
publicclassMediaFile:IMediaFile
{
publicstringFilePath{get;set;}
}
给 OperationMain类一个构造函数,因为Unity有一个构造函数注入,调用代码如下:
staticUnityContainercontainer=newUnityContainer();
staticvoidinit()
{
container.RegisterType();
container.RegisterType();
}
staticvoidMain(string[]args)
{
init();
OperationMainop1=container.Resolve();
op1.PlayMedia();
OperationMainop3=container.Resolve();
op3.PlayMedia();
//普通方式
OperationMainop2=newOperationMain(newPlayer(),newMediaFile());
op2.PlayMedia();
Console.Read();
}
看出来吧,Unity的功能远不止这些,你可以初始化时注册N多,以后直接使用,而不用使用new,还有实例周期的控制、配置文件等灵活控制,具体可以看看Unity(具体不是本节的范畴)的说明。
具体依赖注入划分为三种形式,即构造器注入、属性(设置)注入和接口注入,习惯将其划分为一种(类型)匹配和三种注入:
类型匹配(Type Matching):虽然我们通过接口(或者抽象类)来进行服务调用,但是服务本身还是实现在
某个具体的服务类型中,这就需要某个类型注册机制来解决服务接口和服务类型之间的匹配关系;
构造器注入(Constructor Injection):IoC容器会智能地选择选择和调用适合的构造函数以创建依赖的对象。如果被选择的构造函数具有相应的参数,IoC容器在调用构造函数之前解析注册的依赖关系并自行获得相应参数对象;
属性注入(Property Injection):如果需要使用到被依赖对象的某个属性,在被依赖对象被创建之后,IoC容器会自动初始化该属性;
方法注入(Method Injection):如果被依赖对象需要调用某个方法进行相应的初始化,在该对象创建之后,IoC容器会自动调用该方法。
用配置完成注册
具体实现代码:
UnityConfigurationSectionconfiguration=ConfigurationManager.GetSection(UnityConfigurationSection.SectionName)
asUnityConfigurationSection;
configuration.Configure(container,"defaultContainer");