以前看過前輩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),千萬不要將依賴注入、依賴倒置、控制反轉搞混淆了,依賴倒置是徹底不一樣的原理,雖然它也能夠提供類之間的鬆散耦合和反轉依賴項。文中如有錯誤之處,還望指出,感謝您的閱讀,謝謝。