以前看过前辈Artech《https://www.cnblogs.com/artech/》关于控制反转的一篇文章,文章通俗易懂且言语精炼,写博客既是积累也是分享,既然是分享那么必须让读者可以明白到底讲解的什么,因此在这里我也挑战下本身,看看能不能将概念经过简洁代码和语言的形式充分阐述清楚,如有错误之处,还望指正。面试
控制反转的英文名为Inversion Of Control,咱们简称为IOC,控制反转是一个原则而不是一个设计模式,它是反转程序的控制流,这个术语在Steapano Mazzocchi的Apache软件基金会项目Avalon中被推广,而后在2004年由Robert C. Martin和Martin Fowler进一步推广。正如Martin Fowler所说:控制反转是框架的共同特征,所以说这些轻量级容器之因此特别是由于它们使用控制反转,就好像在说个人车很特别,由于它带有轮子同样。它基本上是框架的定义特征,控制反转用于增长程序的模块化并使其可扩展。那么问题来了,真正反转体如今哪里呢?在早期计算机软件,命令行用于通用程序,所以用户界面由应用程序自己控制,在程序中,咱们能够经过将响应输入命令行来直接控制程序的流程,可是在GUI程序中,咱们基本上是将控件移交给了窗口系统(UI框架),而后由窗口系统决定下一步要作什么,此时程序的主控件从咱们移到了UI框架。控制反转是库和框架之间的区别,使用库时,库本质上是调用特定的函数和方法来执行计算和操做,每一个调用都会完成一些工做,并将控制权返回到客户端,而框架会为咱们完成一些工做,咱们只须要向框架不一样位置注册咱们所编写的代码,而后,框架将在须要时调用咱们编写的代码。用更加通俗易懂的话理解则是:不要叫我,我会叫你或者不要给咱们打电话,咱们会通知你(好莱坞法则)。有了对概念的初步理解,接下来咱们经过代码的形式来加深对概念的理解。设计模式
/// <summary> /// 车引擎类 /// </summary> public class Engine { } /// <summary> /// 汽车类 /// </summary> public class Car { private Engine engine; public Car() { engine = new Engine(); } }
咱们反观上述代码,由于汽车的组成离不开引擎构造,当咱们调用汽车对象实例时,将主动去构造引擎对象实例,表述上没有任何问题,可是咱们意识到引擎和汽车紧密结合在了一块儿,若是构造引擎对象一旦发生变化,毫无疑问咱们须要修改汽车对象,也就是说汽车对象强依赖引擎对象,如今咱们将代码进行以下修改:框架
/// <summary> /// 汽车类 /// </summary> public class Car { private Engine _engine; public Car( Engine engine) { _engine = engine; } }
在此种状况下,汽车对象并不知道如何构造引擎对象,当调用汽车时,汽车的调用者有责任和义务将引擎对象实例传递给汽车,此时流程控制被反转,这种反转相似于基于事件的处理机制。也就是说流程管理从应用程序转移到了框架,通过如此修改后,引擎上升到了框架,如黑匣子通常,由于咱们并不关心引擎具体如何构造。同时咱们也可看出,经过控制反转使程序更加灵活和松散耦合。讲完了控制反转的概念和例子,咱们彷佛还有一个未进行讲解,好像咱们听到更多的是依赖注入,那么依赖注入和控制反转有着怎样的联系呢?依赖注入和控制反转两个相关但概念大相径庭,依赖注入的思想就是一个单独对象,说白了就是编写类的方式,使得能够在构造时将类或函数的特定实例传递给它们,依赖注入其实就意味着控制反转,由于当咱们在对象上调用方法时,它们再也不定位它们所需的其余对象。取而代之的是,它们在构造时就已被赋予了依赖关系,但咱们仍然必须管理构造,经过使用控件容器的反转,咱们可使依赖注入更进一步,经过反转控制容器,咱们只需预先注册全部可用的类。当容器须要构造一个类的实例时,它能够检查该类的构造函数须要哪些对象,而后能够从向其注册的类中构造适当的实例,总的来讲依赖注入只是实现控制反转的一种方式而已。咱们抛开依赖注入实现了控制反转,仅仅只讨论依赖注入带来了哪些好处。ide
既然是面向对象的语言,那么咱们是编写基于面向对象的代码,那么对象天然而然就有其生命周期,有的对象可能咱们只须要一个实例,有的对象可能在程序运行整个过程当中一直存在也就是全局实例,并且有的对象里面存在着对其余对象的引用,如此一来会形成什么问题呢?致使代码难以理解并且难以更改,尤为是对于全局实例而言,全局实例离散性行为太强,分散在整个项目中的各个角落,最主要的是咱们所编写的代码细节中也隐藏了对象之间的交互,有些实例就包含了对其余实例的引用,一旦出现问题,咱们惟有通读每一行代码。咱们经过引入依赖注入代替全局实例方式,经过依赖注入经常使用方式即构造函数注入注入依赖项参数,此举将提升代码的可读性,咱们只需快速浏览构造函数便可查看对应依赖关系。经过引入依赖注入咱们须要注意的是对对应类进行合理划分,由于每次引入新的依赖项时,可能仍是存在类与类之间的依赖,将不一样行为划分到不一样组,如此才能减小类与类之间的耦合,使得咱们的设计更具凝聚力。经过引入依赖注入也使得咱们在进行单元测试时更加方便,由于咱们可经过隔离类来直接测试类实例。模块化
接下来咱们讨论下如何利用程序实现控制反转,实现控制反转最多见的两种方式则是:服务定位器模式(SL)和依赖注入模式(DI)。接下来咱们经过例子利用依赖注入和服务定位器模式实现控制反转。咱们经过控制台实现获取图书馆库图书列表,查询咱们想要的图书,以下咱们定义图书类:函数
public class Book { /// <summary> /// /// </summary> public int Id { get; set; } /// <summary> /// /// </summary> public string Title { get; set; } /// <summary> /// /// </summary> /// <param name="arg"></param> /// <returns></returns> public bool GetAuthor(string arg) { return Title.Equals(arg); } }
而后接下来咱们将控制台程序名称修改成图书馆库,而后根据咱们输入的图书来查询图书并打印,伪代码以下:单元测试
class Library { static void Main(string[] args) { var books = bookFinder.FindAll();
foreach (var book in books) { if (!book.GetAuthor(args[0])) continue; Console.WriteLine(book.Title); }; Console.ReadKey(); } }
如上咱们经过bookFinder获取图书馆图书列表,而后查询咱们输入的图书名称并打印,咱们一眼就能看出这个bookFinder从哪里来呢?咱们可能查找深圳图书馆或者国家图书馆或者网上远程爬取呢?,因此接下来咱们须要建立bookFinder的接口实现,以下:测试
/// <summary> /// 查询图书列表 /// </summary> public interface IBookFinder { List<Book> FindAll(); }
/// <summary> /// 深圳图书馆库 /// </summary> public class ShenZhenLibraryBookFinder : IBookFinder { public List<Book> FindAll() { ...... } } public class Library { private IBookFinder _bookFinder; public Library() { _bookFinder = new ShenZhenLibraryBookFinder(); } public IEnumerable<Book> BooksAuthoredBy(string title) { var allBooks = _bookFinder.FindAll(); foreach (var book in allBooks) { if (!book.GetAuthor(title)) continue; yield return book; } } }
通过上述改造后,咱们提供了IBookFinder接口以及其实现,可是如今咱们正在将其做为一个框架,须要被其余人可扩展和使用,若此时须要提供给国家图书馆使用呢?咱们能够看到此时图书库即Library同时依赖IBookFinder和及其实现,当咱们做为可扩展框架时,最佳效果则是依赖接口而不是依赖具体实现细节,那么此时该实例咱们到底该如何使用呢?答案则是控制反转,咱们经过依赖注入实现控制反转。ui
public class BookFinder { public IBookFinder ProvideShenZhenBookFinder() { return new ShenZhenLibraryBookFinder(); } public IBookFinder ProvideNationalBookFinder() { return new NationalLibraryBookFinder(); } }
/// <summary> /// 国家图书馆库 /// </summary> public class NationalLibraryBookFinder : IBookFinder { public List<Book> FindAll() { Console.WriteLine("欢迎来到国家图书馆!"); return new List<Book>() { new Book() { Id = 1, Title = "策略思惟" } }; } } /// <summary> /// 深圳图书馆库 /// </summary> public class ShenZhenLibraryBookFinder : IBookFinder { public List<Book> FindAll() { Console.WriteLine("欢迎来到深圳图书馆!"); return new List<Book>() { new Book() { Id = 1, Title = "月亮和六便士" } }; } }
接下来咱们将上述图书馆库Library修改成经过构造函数注入IBookFinder接口,此时库将仅仅只依赖于IBookFinder接口,IBookFinder内部具体实现Library并不关心,而后在控制台进行以下调用:this
var bookFinder = new BookFinder(); var shenzhenBookFinder = new Library(bookFinder.ProvideShenZhenBookFinder()); var books = shenzhenBookFinder.BooksAuthoredBy(args[0]);
上述咱们经过依赖注入使得咱们能够进行可扩展,根据不一样图书馆须要只需提供IBookFinder具体实现便可,依赖注入并非实现控制反转惟一的方式,咱们还能够经过服务定位器来实现,服务定位器的背后是一个对象,该对象知道如何获取应用程序可能须要的全部服务,也就是说服务定位器提供咱们返回IBookFinder接口的实现,以下:
/// <summary> /// 服务定位器 /// </summary> public class ServiceLocator { /// <summary> /// 存储或获取注册服务 /// </summary> private IDictionary<string, object> services = new Dictionary<string, object>(); private static ServiceLocator _serviceLocator; public static void Load(ServiceLocator serviceLocator) { _serviceLocator = serviceLocator; } /// <summary> /// 获取服务 /// </summary> /// <param name="key"></param> /// <returns></returns> public static object GetService(string key) { _serviceLocator.services.TryGetValue(key, out var service); return service; } /// <summary> /// 加载服务 /// </summary> /// <param name="key"></param> /// <param name="service"></param> public void LoadService(string key, object service) { services.Add(key, service); } }
ServiceLocator locator = new ServiceLocator(); locator.LoadService(nameof(ShenZhenLibraryBookFinder), new ShenZhenLibraryBookFinder()); locator.LoadService(nameof(NationalLibraryBookFinder), new NationalLibraryBookFinder()); ServiceLocator.Load(locator); var finder = (IBookFinder)ServiceLocator.GetService(nameof(ShenZhenLibraryBookFinder)); var shenzhenBookFinder = new Library(finder); var books = shenzhenBookFinder.BooksAuthoredBy(args[0]);
经过依赖注入和服务定位器实现控制反转都分离了相互依赖,只不过依赖注入让咱们经过构造函数一目了然就可查看依赖关系,而服务定位器须要显式请求依赖关系,本质上没有任何区别,至于如何使用,主要取决于咱们对两者的熟悉程度。正如Martin Fowler所说:使用服务定位器时,每一个服务都依赖于服务定位器,它能够隐藏对其余实现的依赖关系,可是咱们确实须要查看服务定位器,所以,是否采用定位器仍是注入器主要决定于该依赖关系是否成问题。讲到这里咱们借助于IServiceProvider接口实现.NET Core中的服务定位器。以下:
public class ServiceLocator { public static IServiceProvider Instance; }
除了以上写法外,咱们还能够经过实例化ServiceLocator的方式来获取服务,以下:
public class ServiceLocator { private IServiceProvider _currentServiceProvider; private static IServiceProvider _serviceProvider; public ServiceLocator(IServiceProvider currentServiceProvider) { _currentServiceProvider = currentServiceProvider; } public static ServiceLocator Current { get { return new ServiceLocator(_serviceProvider); } } public static void SetLocatorProvider(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public T GetService<T>() { return _currentServiceProvider.GetRequiredService<T>(); } } /// <summary> /// IServiceProvider扩展方法 /// </summary> public static class ServiceProviderExtensions { public static T GetRequiredService<T>(this IServiceProvider provider) { var serviceType = typeof(T); if (provider is ISupportRequiredService requiredServiceSupportingProvider) { return (T)requiredServiceSupportingProvider.GetRequiredService(serviceType); } var service = (T)provider.GetService(serviceType); if (service == null) { throw new InvalidOperationException($"{serviceType} no registered"); } return service; } }
接下来咱们写一个简单的接口来验证是否正确:
public interface IHelloWorld { string Say(); } public class HelloWorld : IHelloWorld { public string Say() { return "Hello World"; } }
不知道上述两种写法是否存在有什么不妥的地方,有的时候经过服务定位器的方式也很是清爽,由于当咱们实例化最终具体实现时经过构造注入依赖项时,本没有什么,可是若后期一旦须要增长或减小依赖项时,咱们一样须要修改最终具体实现,像这种状况是否能够考虑用服务定位器模式,直接经过服务定位器去获取指定服务,当在具体方法里时咱们每次都得去获取服务,反而不如在构造器中一劳永逸注入。因此选择注入器和定位器根据我的而选择或者根据具体功能实现而定才是最佳。
上述咱们经过代码的形式来进一步阐述了控制反转,在代码的世界里,咱们运用控制反转游刃有余,在现实生活里,咱们运用控制反转也是驾轻就熟。年底将至,全家欢聚一堂,这应该是一年中最热闹的一次家庭聚会了吧,为了准备年饭具体要提供哪些食材和食物做为家庭的一份子都得有基本了解,因此咱们必须提早准备好这些,这就像咱们编写一个没有依赖注入的基本程序同样,这是在自家作的状况,自家作饭吃完后,又不能抹抹嘴上油,拍拍屁股立刻走人,还得收拾不是,因而乎咱们将年饭地点切换到饭店进行,此时饭店相似取缔了咱们自备食材这一块,饭店就像餐饮服务商同样,咱们不用本身作,饭店会给咱们提供食物,它会根据咱们的不一样需求注入不一样的餐饮服务。从自家-》饭店,整个流程控制权进行反转,咱们将年饭控制权交给了饭店,由于饭店成为了年饭这一事件的策划者,它是咱们能不能成功吃上年饭的必要条件,咱们告诉饭店老板:有几我的、带了小孩、口味需重一点等等,咱们须要作的就是提供一些基本参数,而后饭店自会组织,咱们并不须要关心和干涉细节,他们会处理全部问题,一切就绪后会通知咱们。
写本文的目的是一直对控制反转和依赖注入不太理解,在脑海中一直处于模糊的概念,同时呢,以前面试官问我关于依赖注入的理解,我竟然支支吾吾的说成依赖倒置原则(Dependency Inversion Principle),千万不要将依赖注入、依赖倒置、控制反转搞混淆了,依赖倒置是彻底不一样的原理,虽然它也能够提供类之间的松散耦合和反转依赖项。文中如有错误之处,还望指出,感谢您的阅读,谢谢。