我们首先通过一段小故事来了解为什么要使用IOC。
有一天我的老师燕小燕走进课堂,在黑板上写了一段程序,我有一台电脑,但是不能播放音乐,你们能不能写一个驱动让其具备播放音乐的功能。
1 /// <summary> 2 /// 我的电脑 3 /// </summary> 4 public class Computer 5 { 6 7 }
冥思苦想后我开发了一个多媒体驱动程序用于识别并播放mp3。
1 /// <summary> 2 /// 我的电脑 3 /// </summary> 4 public class Computer 5 { 6 MediaDriver driver = new MediaDriver(); 7 8 /// <summary> 9 /// 播放 10 /// </summary> 11 public void PlayMedia() 12 { 13 driver.LoadMedia(new Mp3()); 14 driver.Play(); 15 } 16 } 17 18 /// <summary> 19 /// 电脑多媒体驱动 20 /// </summary> 21 public class MediaDriver 22 { 23 private Mp3 _mp3 = null; 24 public void LoadMedia(Mp3 mp3) 25 { 26 _mp3 = new Mp3(); 27 //dosomething 28 } 29 public void Play() 30 { 31 _mp3.Play(); 32 } 33 } 34 35 /// <summary> 36 /// mp3文件 37 /// </summary> 38 public class Mp3 39 { 40 /// <summary> 41 /// 播放 42 /// </summary> 43 public void Play() 44 { 45 //播放音乐 46 } 47 }
于是我便迫不及待向老师展示我的成果,老师看后首先夸奖了我一番,但是随即向我提出来一个问题,你的这个驱动写的确实是没问题,但是如果我以后想观看mp4视频该怎么办呢?
我:我可以再编写一个支持播放mp4的驱动程序啊!
燕小燕:那如果后面还有其他的媒体文件该怎么办呢?你总不可能每个媒体文件的播放都需要一个驱动程序吧,那最后这个电脑上安装的都是各种媒体的驱动程序。现在你的问题是你的驱动中严重依赖了Mp3的类。
我:嗯,有道理。
由于mp3和mp4都是媒体文件,所以我对他们分析后抽象出了一个IMedia的接口。于是乎我对驱动进行了改造。
1 /// <summary> 2 /// 我的电脑 3 /// </summary> 4 public class Computer 5 { 6 MediaDriver driver = new MediaDriver(); 7 8 /// <summary> 9 /// 播放 10 /// </summary> 11 public void PlayMedia() 12 { 13 driver.LoadMedia(new Mp3()); 14 driver.Play(); 15 16 driver.LoadMedia(new Mp4()); 17 driver.Play(); 18 } 19 } 20 21 /// <summary> 22 /// 电脑多媒体驱动 23 /// </summary> 24 public class MediaDriver 25 { 26 private IMedia _media = null; 27 public void LoadMedia(IMedia media) 28 { 29 _media = media; 30 //dosomething 31 } 32 public void Play() 33 { 34 _media.Play(); 35 } 36 } 37 38 /// <summary> 39 /// 媒体接口 40 /// </summary> 41 public interface IMedia 42 { 43 void Play(); 44 } 45 46 /// <summary> 47 /// mp3文件 48 /// </summary> 49 public class Mp3: IMedia 50 { 51 /// <summary> 52 /// 播放 53 /// </summary> 54 public void Play() 55 { 56 //播放音乐 57 } 58 } 59 60 /// <summary> 61 /// mp4文件 62 /// </summary> 63 public class Mp4: IMedia 64 { 65 /// <summary> 66 /// 播放 67 /// </summary> 68 public void Play() 69 { 70 //播放视频 71 } 72 }
老师:嗯,不错,这样的话,不管以后还有其他的媒体文件,只要继承自IMedia,那么就可以使用这个驱动进行播放了。但是还有一个问题,每次播放需要手动在电脑类中进行实例化mp3mp4类非常的不方便,万一我们要播放mp5,那不可能把电脑拆掉然后修改他的PalyMedia方法吧,换句话说,你的电脑类严重依赖了mp3、mp4的类。
为了解耦电脑对于Mp3类的直接依赖,于是我想能不能电脑只是发起一个指令表达自己需要什么对象,不需要关心对象从何而来,由其他的组件把需要的实例化对象返回给他,那么电脑需要什么告诉这个组件就行了,但是为了让这个组件知道要实例化什么对象返回给我,那我也肯定要在初始化这个组件的时候告诉他我需要的对象与实例化的对象之间的关系,所以必须有一个关系注册的环节,带着这些思考于是我又对代码做了一些修改。
1 /// <summary> 2 /// 我的电脑 3 /// </summary> 4 public class Computer 5 { 6 private MediaDriver _driver; 7 public Computer() 8 { 9 _driver = new MediaDriver(); 10 IocContainer.Instance.Register(); 11 } 12 13 /// <summary> 14 /// 播放 15 /// </summary> 16 public void PlayMedia(IMedia media) 17 { 18 _driver.LoadMedia(media); 19 _driver.Play(); 20 } 21 22 public interface IMP3 : IMedia { } 23 public interface IMP4 : IMedia { } 24 25 /// <summary> 26 /// mp3文件 27 /// </summary> 28 public class Mp3: IMedia,IMP3 29 { 30 /// <summary> 31 /// 播放 32 /// </summary> 33 public void Play() 34 { 35 //播放音乐 36 Console.WriteLine("播放音乐"); 37 } 38 } 39 40 /// <summary> 41 /// mp4文件 42 /// </summary> 43 public class Mp4: IMedia, IMP4 44 { 45 /// <summary> 46 /// 播放 47 /// </summary> 48 public void Play() 49 { 50 //播放视频 51 Console.WriteLine("播放视频"); 52 } 53 } 54 55 /// <summary> 56 /// IOC容器 57 /// </summary> 58 public class IocContainer 59 { 60 private static IocContainer _container; 61 private Dictionary<Type, Type> _regDic; 62 public static IocContainer Instance 63 { 64 get 65 { 66 if(_container==null) 67 { 68 _container = new IocContainer(); 69 _container._regDic = new Dictionary<Type, Type>(); 70 71 } 72 return _container; 73 } 74 } 75 private IocContainer() { } 76 77 public void Register() 78 { 79 _container.Register<IMP3, Mp3>(); 80 _container.Register<IMP4, Mp4>(); 81 } 82 83 /// <summary> 84 /// 获取实例对象 85 /// </summary> 86 /// <typeparam name="T"></typeparam> 87 /// <returns></returns> 88 public T GetService<T>() where T : class 89 { 90 if (_regDic.TryGetValue(typeof(T), out Type value)) 91 //生成T的实例对象返回 92 return (T)Activator.CreateInstance(value); 93 return null; 94 } 95 96 /// <summary> 97 /// 注册 98 /// </summary> 99 /// <typeparam name="T"></typeparam> 100 /// <typeparam name="Implement"></typeparam> 101 public void Register<T, Implement>() 102 where T : class 103 where Implement : T 104 { 105 _regDic.Add(typeof(T), typeof(Implement)); 106 } 107 }
我:现在可以了,Computer类不再直接依赖于mp3类,而是直接依赖IocContainer类,需要获取mp3的实例对象时只需使用IocContainer.Instance.GetService<IMP3>()即可得到。
其实到现在为止,上面的IocContainer类就算是一个简单的IOC容器了。我们通过这个IOC容器实现了控制反转,让电脑类和驱动类不直接依赖mp3、mp4等多种媒体类型,而是依赖外部的容器,实现了中上层程序对下层程序的解耦。
再回过头来,我们看一下什么是IOC?
IOC(Inversion of Control)即控制反转,它是一种编程思想,是一个重要的面向对象编程的法则。用来消减程序之间的耦合问题,把中上层程序内部对下层依赖,转移到一个外部来装配。
IOC是程序设计的目标,实现方式包含依赖注入和依赖查找,在.net中只有依赖注入。
IOC容器主要用于解决中上层对下层的直接依赖。让程序中上层对下层控制权从主动转为被动,程序从原来的主动生成需要对象转为只需告诉自己需要什么对象,而不管对象生成的具体实现,一切由IOC自动生成并返回程序需要的对象。除了控制反转实现解耦,同时还实现了依赖注入(DI),可以看到我们上面生成实例对象是通过IocContainer.Instance.GetService<>()的方式手动生成,而通过IOC容器的注入,开发人员可以通过注入的方式实现自动生成实例对象。
一个完整的IOC容器会为我们实现控制反转、依赖注入还有生命周期的管理。
依赖注入有三种形式:构造函数注入、属性注入、接口注入。用的最多的就是构造函数注入,其他两种用的较少。
上面编写的代码只是为了讲解控制反转的思想,不恰当和不完善的地方希望大家不要较真。这里也不再过多介绍容器的用法了,目前IOC容器已经有很多现成可以使用的,比如用的比较多的就是Unity和Autofac,大家可以网上自行搜索。