1.前言
在讲《设计模式》前,还有一个实现技巧说一下。它就是依赖注入。
为什么要介绍它?
面向抽象(接口)编程是抓住“依赖倒置原则”(后续文章会介绍) 的核心。
依赖倒置是站在客户程序角度来看的,客户程序依赖的是“相对稳定”的接口,而不是“相对多的”子类。也就是客户程序不要依赖子类。
设计原则还有一个“里氏替换原则”,它是站在模式对象角度来看的,模式对象将“相对多变”的子类视同它的接口(或父类)。也就是父类出现的地方,子类可以代替。
说到“倒置”,可能晕乎。那么先说“正置”吧。依赖正置就是类间的依赖是实实在在的实现类间的依赖,也就是面向实现编程,也是人的通常的思维方式。我要使用电脑就得有电脑,依赖它。而编写程序需要的是对现实世界的事物进行抽象,这样就形成了抽象类或接口。系统设计一般依赖抽象,这样代替人的思维中的事物间的依赖,倒置也就产生了。
可是抽象的事物,是具体的事物的模板,终归于依靠具体实现的。该怎样使得客户程序不依赖的具体类型,可以使用依赖注入方式。
哎!解释得很绕了。抱歉!迷糊也罢。演示实例过程中再领悟啦。
2.依赖注入概述
客户程序依赖某个对象(或类),一般对它进行抽象,形成抽象类或接口,这样客户程序可以摆脱所依赖的具体类型。
依赖注入(Dependency Injection,简称DI),也可以叫控制反转(Inverse Of Control,简称IOC)。两种术语,本质是一样的。
3.案例场景
客户程序需要获取当前的时间。先创建一个时间提供者类,其代码:
//时间提供者类 public class TimeProvider { public DateTime CurrentDate { get { return DateTime.Now; } } }
在客户程序(Web程序)使用:
[Route("api/[controller]")] public class TimeProviderController : Controller { //实例化 TimeProvider tp = new TimeProvider(); [HttpGet] public string Get() { return tp.CurrentDate.ToString(); } }
这样客户程序依赖的是具体的TimeProvider类型了。我们进行抽象,并实现它。
创建接口:
//时间提供者 接口类 public interface ITimeProvider { DateTime CurrentDate { get; } }
实现:
//时间提供者 实现类 public class SystemTimeProvider : ITimeProvider { public DateTime CurrentDate { get { return DateTime.Now; } } }
客户程序:
public class TimeProviderController : Controller { //实例化 //TimeProvider tp = new TimeProvider(); //tp变量表面类型是ITimeProvider,实际类型还是SystemTimeProvider //TODO:客户程序没有摆脱具体的SystemTimeProvider类型 ITimeProvider tp = new SystemTimeProvider(); [HttpGet] public string Get() { return tp.CurrentDate.ToString(); } }
问题来了,客户程序该如何摆脱具体的SystemTimeProvider类型依赖?
4.反射方式
引入一个中间类,来装配类型:
//装配类 public class Assembler { //保存抽象类型与具体类型对应关系的字典 static Dictionary<Type, Type> d = new Dictionary<Type, Type>(); static Assembler() { //注册抽象类型需要的具体类型 //TODO:可以通过配置文件来定义 d.Add(typeof(ITimeProvider), typeof(SystemTimeProvider)); } private static object Create(Type type) { if ((type == null) || !d.ContainsKey(type)) { throw new NullReferenceException(); } return Activator.CreateInstance(d[type]); } public static T Create<T>() { return (T)Create(typeof(T)); } }
客户程序:
public class TimeProviderController : Controller { //实例化 //TimeProvider tp = new TimeProvider(); //tp变量表面类型是ITimeProvider,实际类型还是SystemTimeProvider //TODO:客户程序没有摆脱具体的SystemTimeProvider类型 //ITimeProvider tp = new SystemTimeProvider(); //通过装配类的方法来实例化,这样摆脱了具体类型依赖 ITimeProvider tp = Assembler.Create<ITimeProvider>(); [HttpGet] public string Get() { return tp.CurrentDate.ToString(); } }
客户程序只需依赖接口ITimeProvider和组配类Assembler,无须知道具体类型SystemTimeProvider的存在。其类图的静态结构:
注:Assembler相对于使用DI框架,功能弱爆了。当然,也可以使用工厂模式(后续文章介绍)来替代。
5.依赖注入框架
DI框架都是围绕一个容器对象构建的,当容器对象绑定到某些配置信息(或方法调用)时,它就会解析依赖性。
较为流行的DI框架:Autofac,CatleWindsor,Ninject,Sprint.Net,StructureMap,Untity,MEF等。
下面使用MVC6(EntityFramework7也有,或者单独引入)包含的DI框架:
在Startup.cs中ConfigureServices方法配置:
public void ConfigureServices(IServiceCollection services) { //TODO:可以通过配置文件来定义 //services.AddScoped<ITimeProvider, SystemTimeProvider>(); //单例模式 services.AddSingleton<ITimeProvider, SystemTimeProvider>(); //services.AddInstance<ITimeProvider>(new SystemTimeProvider()); services.AddMvc(); }
6.构造方式
客户程序:
public class TimeProviderController : Controller { private readonly ITimeProvider tp; public TimeProviderController(ITimeProvider _tp) { this.tp = _tp; } [HttpGet] public string Get() { return tp.CurrentDate.ToString(); } }
7.设值方式
客户程序:
public class TimeProviderController : Controller { //private readonly ITimeProvider tp; //public TimeProviderController(ITimeProvider _tp) //{ // this.tp = _tp; //} public ITimeProvider tp { get; set; } [HttpGet] public string Get() { return tp.CurrentDate.ToString(); } }
代码格式是这样的,也就是不用构造函数。但目前ASP.NET5自带的DI还不能使用设值属性方式注入。
还用组配类来匹配,不用DI框架:
public class TimeProviderController : Controller { public ITimeProvider tp { get; set; } //装配类来获取类型实例化,没有用到DI框架 public TimeProviderController() { tp = Assembler.Create<ITimeProvider>(); } [HttpGet] public string Get() { return tp.CurrentDate.ToString(); } }
8.本章小结
关于依赖注入方式,还有接口注入方式,特性标注注入方式等,这些不常用,不扯了。
依赖注入也可以称为一个设计模式,但就象“前言”所说,当作实现技巧或者设计技巧而已。
常用的构造注入和设值注入方式,要掌握啦。两者区别?
构造注入是一次性的,当客户类型构造的时候就确定了。它适合那种生命周期不长,在其存续期间不需要重新适配的对象。
设值方式是对于生命周期较长的客户对象而言,可以在运行过程中随时注入,较为灵活。
9.附录
以上例子源码方案目录:
说明:
GiveCase.Modeling是建模设计
GiveCase.Web是表现层
GiveCase.Service是业务层
其下载地址,到 290576772 QQ群空间下载。