不要叫我,我會叫你(控制反轉原理)


前言

之前看過前輩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;
        }
    }

在此種情況下,汽車對象並不知道如何構造引擎對象,當調用汽車時,汽車的調用者有責任和義務將引擎對象實例傳遞給汽車,此時流程控制被反轉,這種反轉類似於基於事件的處理機制。也就是說流程管理從應用程序轉移到了框架,經過如此修改后,引擎上升到了框架,如黑匣子一般,因為我們並不關心引擎具體如何構造。同時我們也可看出,通過控制反轉使程序更加靈活和松散耦合。講完了控制反轉的概念和例子,我們似乎還有一個未進行講解,好像我們聽到更多的是依賴注入,那么依賴注入和控制反轉有着怎樣的聯系呢?依賴注入和控制反轉兩個相關但概念截然不同,依賴注入的思想就是一個單獨對象,說白了就是編寫類的方式,使得可以在構造時將類或函數的特定實例傳遞給它們,依賴注入其實就意味着控制反轉,因為當我們在對象上調用方法時,它們不再定位它們所需的其他對象。取而代之的是,它們在構造時就已被賦予了依賴關系,但我們仍然必須管理構造,通過使用控件容器的反轉,我們可以使依賴注入更進一步,通過反轉控制容器,我們只需預先注冊所有可用的類。當容器需要構造一個類的實例時,它可以檢查該類的構造函數需要哪些對象,然后可以從向其注冊的類中構造適當的實例,總的來說依賴注入只是實現控制反轉的一種方式而已。我們拋開依賴注入實現了控制反轉,僅僅只討論依賴注入帶來了哪些好處。

 

既然是面向對象的語言,那么我們是編寫基於面向對象的代碼,那么對象自然而然就有其生命周期,有的對象可能我們只需要一個實例,有的對象可能在程序運行整個過程中一直存在也就是全局實例,而且有的對象里面存在着對其他對象的引用,如此一來會造成什么問題呢?導致代碼難以理解而且難以更改,尤其是對於全局實例而言,全局實例離散性行為太強,分散在整個項目中的各個角落,最主要的是我們所編寫的代碼細節中也隱藏了對象之間的交互,有些實例就包含了對其他實例的引用,一旦出現問題,我們唯有通讀每一行代碼。我們通過引入依賴注入代替全局實例方式,通過依賴注入常用方式即構造函數注入注入依賴項參數,此舉將提高代碼的可讀性,我們只需快速瀏覽構造函數即可查看對應依賴關系。通過引入依賴注入我們需要注意的是對對應類進行合理划分,因為每次引入新的依賴項時,可能還是存在類與類之間的依賴,將不同行為划分到不同組,如此才能減少類與類之間的耦合,使得我們的設計更具凝聚力。通過引入依賴注入也使得我們在進行單元測試時更加方便,因為我們可通過隔離類來直接測試類實例。

控制反轉代碼說明 

接下來我們討論下如何利用程序實現控制反轉,實現控制反轉最常見的兩種方式則是:服務定位器模式(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和及其實現,當我們作為可擴展框架時,最佳效果則是依賴接口而不是依賴具體實現細節,那么此時該實例我們到底該如何使用呢?答案則是控制反轉,我們通過依賴注入實現控制反轉。

    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並不關心,然后在控制台進行如下調用:

            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),千萬不要將依賴注入、依賴倒置、控制反轉搞混淆了,依賴倒置是完全不同的原理,雖然它也可以提供類之間的松散耦合和反轉依賴項。文中若有錯誤之處,還望指出,感謝您的閱讀,謝謝。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM