[ASP.NET Core 3框架揭秘] 依賴注入:控制反轉
- 2019 年 10 月 16 日
- 筆記
ASP.NET Core框架建立在一些核心的基礎框架之上,這些基礎框架包括依賴注入、文件系統、配置選項和診斷日誌等。這些框架不僅僅是支撐ASP.NET Core框架的基礎,我們在進行應用開發的時候同樣會頻繁地使用到它們。對於這裡提到的這幾個基礎框架,依賴注入尤為重要。ASP.NET Core應用在啟動以及後續針對請求的處理過程中,它會依賴各種的組件提供服務。為了便於訂製,這些組件一般會以介面的形式進行「標準化」,我們將這些標準化的組件統一稱為「服務(Service)」。整個ASP.NET Core框架建立在一個底層的依賴注入框架之上,它使用依賴注入容器來提供所需的服務對象。要了解這個依賴注入容器以及它的服務提供機制,我們得先知道什麼是「依賴注入(DI:Dependence Injection)」。一旦我們提到依賴注入,又不得不說說「控制反轉(IoC:Inverse of Control)」。
一、流程式控制制的反轉
軟體開發中的一些所謂的設計理念往往沒有一個明確的定義,比如之前流行的SOA和現在炒得火熱的「微服務(Micro Service)」和「無伺服器(Serverless)」,我們都沒法通過一個明確的「內涵」給它們一個準確的定義,而只能從「外延」上描述這些架構設計應該具有怎樣的特性。正因為無法給出一個明確的界定,這造成了人們針對同一個概念往往會有很多不同的理解。針對IoC也是這種情況,所以本章所述的僅僅代表作者的一家之言,讀者朋友姑且聽之,僅作參考。
我聽到很多人將IoC說成是一種「面向對象的設計模式」,但在我個人看來IoC不但不能算作一種「設計模式」,其自身也與「面向對象」沒有直接的關係。很多人之所以不能很準確地理解IoC,緣於他們忽略了一個最根本的東西,那就是IoC這個短語本身。
IoC的全名Inverse of Control,翻譯成中文就是「控制反轉」或者「控制倒置」。控制反轉也好,控制倒置也罷,它體現的意思是控制權的轉移,即控制權原來在A手中,現在需要B來接管。那麼對於軟體設計來說,IoC所謂的控制權轉移具有怎樣的體現呢?要回答這個問題,就需要先了解IoC的C(Control)究竟指的是怎樣一種控制。對於我們所在的任何一項任務,不論其大小,基本上都可以分解成相應的步驟,所以任何一項任務的實施都有其固有的流程,而IoC涉及的控制可以理解為「針對流程的控制」。
我們通過一個具體實例來說明傳統的設計在採用了IoC之後針對流程的控制是如何實現反轉的。比如我們要設計一個針對Web的MVC類庫,不妨將其命名為MvcLib。簡單起見,這個類庫中只包含如下這個同名的靜態類。
public static class MvcLib { public static Task ListenAsync(Uri address); public static Task<Request> ReceiveAsync(); public static Task<Controller> CreateControllerAsync(Request request); public static Task<View> ExecuteControllerAsync(Controller controller); public static Task RenderViewAsync(View view); }
MvcLib提供了如上5個方法幫助我們完成整個HTTP請求流程中的5個核心任務。具體來說,ListenAsync方法啟動一個監聽器並將其綁定到指定的地址進行HTTP請求的監聽,抵達的請求通過ReceiveAsync方法進行接收,接收到的請求通過一個Request對象來表示。CreateControllerAsync方法根據接收到的請求解析並激活目標Controller對象。ExecuteControllerAsync方法執行激活的Controller並返回一個表示視圖的View對象。RenderViewAsync最終將View對象轉換成HTML並作為當前請求響應的內容返回給請求的客戶端。
現在我們在這個MvcLib的基礎上創建一個真正的MVC應用。我們會發現除了按照MvcLib的規範自定義具體的Controller和View之外,我們還需要自行控制從請求的監聽與接收、Controller的激活與執行以及View的最終呈現在內的整個流程,這樣一個執行流程反映在如下所示的程式碼中。
class Program { static async Task Main() { while (true) { var address = new Uri("http://0.0.0.0:8080/mvcapp"); await MvcLib.ListenAsync(address); while (true) { var request = await MvcLib.ReceiveAsync(); var controller = await MvcLib.CreateControllerAsync(request); var view = await MvcLib.ExecuteControllerAsync(controller); await MvcLib.RenderViewAsync(view); } } } }
這個例子體現了如下圖所示的流程式控制制方式(應用的程式碼完全採用非同步的方式來處理請求,為了讓流程圖顯得更加簡單,我們在流程圖中畫成了同步的形式,讀者不必糾結這個問題)。我們設計的類庫(MvcLib)僅僅通過API的形式提供各種單一功能的實現,作為類庫消費者的應用程式(App)則需要自行編排整個工作流程。如果從程式碼重用的角度來講,這裡被重用的僅限於實現某個環節單一功能的程式碼,編排整個工作流程的程式碼並沒有得到重用。
但是在真實開發場景下,我們需要的不僅僅是一個能夠提供單一API的類庫,而是能夠直接在上面構建應用的框架。類庫(Library)和框架(Framework)的不同之處在於:前者往往只是提供實現某種單一功能的API,而後者則針對一個目標任務對這些單一功能進行編排形成一個完整的流程,並利用一個引擎驅動這個流程自動執行。
對於我們上面演示的MvcLib來說,作為消費者的應用程式需要自行控制整個HTTP請求的處理流程,但這實際上這是一個很「泛化」的工作流程,幾乎所有的MVC應用均採用這樣的流程來監聽、接收請求並最終對請求予以響應。如果我們將這個流程實現在一個MVC框架之中,由它構建的所有MVC應用就可以直接使用這個請求處理流程,不需要作無謂的DIY(Do It Yourself)。
現在我們將MvcLib從類庫改造成一個框架,姑且將其稱為MvcFrame。如下圖所示,MvcFrame的核心是一個被稱為MvcEngine的執行引擎,它驅動一個編排好的工作流對HTTP請求進行一致性處理。如果我們利用MvcFrame構建一個具體的MVC應用,除了根據我們的業務需求定義相應的Controller和View之外,我們只需要初始化這個引擎並直接啟動它即可。如果你曾經開發過ASP.NET MVC應用,你會發現ASP.NET MVC就是這麼一個框架。
有了前面演示的這個例子作為鋪墊,我們應該很容易理解IoC所謂的控制反轉本質上說的是什麼了。總的來說,IoC是我們設計框架所採用的一種基本思想,所謂的控制反轉就是將應用對流程的控制轉移到框架中。拿前面這個例子來說,在傳統面向類庫編程的時代,針對HTTP請求處理的流程牢牢控制在應用程式手中。在引入框架之後,請求處理的控制權轉移到了框架手中。
二、好萊塢法則
在好萊塢,演員把簡歷遞交給演藝公司後就只有回家等待。由於演藝公司對整個娛樂項目具有完全控制權,演員只能被動地接受電影公司的邀約。「不要給我們打電話,我們會給你打電話(Don『t call us, we『ll call you)」這是著名的好萊塢法則( Hollywood Principle或者 Hollywood Low),IoC完美地體現了這一法則。
在IoC的語境中,框架就像是掌握整個電影製片流程的電影公司,由於它是整個工作流程的實際控制者,所以只有它知道哪個環節需要哪些人員。應用程式就像是演員,它只需要按照框架訂製的規則註冊這些組件就可以了,因為框架會在適當的時機自動載入並執行註冊的組件。
以熟悉的ASP.NET MVC應用開發來說,我們只需要按照約定的規則(比如約定的目錄結構和文件與類型命名方式等)定義相應的Controller類型和View文件就可以了。當ASP.NET MVC框架在處理請求的過程中,它會根據路由解析生成參數得到目標Controller的類型,然後自動創建Controller對象並執行它。如果目標Action方法需要呈現一個View,框架會根據預定義的目錄約定找到對應的View文件(.cshtml文件),並對它實施動態編譯生成對應的類型。當目標View對象創建出來之後,它執行之後生成的HTML會作為響應回復給客戶端。可以看出,整個請求流程處處體現了「框架Call應用」的好萊塢法則。
總的來說,我們在一個框架的基礎上進行應用開發,就相當於在一條調試好的流水線上生產某種商品。我們只需要在相應的環節準備對應的原材料,最終下線的就是我們希望得到的產品。IoC幾乎是所有框架均具有的一個固有屬性,從這個意義上講,「IoC框架」的說法其實是錯誤的,世界上並沒有什麼IoC框架,或者說所有的框架都是IoC框架。
三、流程訂製
我們採用IoC實現了流程式控制制從應用程式向框架的轉移,但是被轉移的僅僅是一個泛化的流程,任何一個具體的應用都可能需要對該流程的某些環節進行訂製。還是以我們的MVC框架來說,默認實現的請求處理流程可以只考慮針對HTTP 1.1的支援,但是我們在設計框架的時候應該提供相應的擴展點來支援HTTP 2。作為一個Web框架,用戶認證功能是必備的,但是框架自身不能限制於某一種或者幾種固定的認證方式,它應該允許我們通過擴展實現任意的認證模式。
我們可以說得更加寬泛點。如下圖所示,我們將一個泛化的工作流程(A=>B=>C)定義在框架之中,建立在該框架的兩個應用需要對組成這個流程的某些環節進行訂製。比如步驟A和C可以被App1重用,但是步驟B卻需要被訂製(B1)。App2則重用步驟A和B,但是需要按照自己的方式處理步驟C。
IoC將對流程的控制從應用程式轉移到框架之中,框架利用一個引擎驅動整個流程的自動化執行。應用程式無需關心工作流程的細節,它只需要啟動這個引擎即可。這個引擎一旦被啟動,框架就會完全按照預先編排好的流程進行工作,如果應用程式希望整個流程按照自己希望的方式被執行,需要在啟動之前對流程進行訂製。一般來說,框架會以相應的形式提供一系列的擴展點,應用程式通過註冊擴展的方式實現對流程某個環節的訂製。在引擎被啟動之前,應用程式將所需的擴展註冊到框架之中。一旦引擎被正常啟動,這些註冊的擴展會自動參與到整個流程的執行過程中。
綜上所述,IoC一方面通過流程式控制制從應用程式向框架的反轉實現了針對流程自身的重用,另一方面通過內置的擴展機制使這個被重用的流程能夠自由地被訂製,這兩個因素決定了框架自身的價值。重用讓框架不僅僅是為應用程式提供實現單一功能的API,而是提供一整套可執行的解決方案,可訂製則使我們可以為不同的應用程式對框架進行訂製,這無疑讓框架可以使用到更多的應用之中。
[ASP.NET Core 3框架揭秘] 依賴注入:控制反轉
[ASP.NET Core 3框架揭秘] 依賴注入:IoC模式
[ASP.NET Core 3框架揭秘] 依賴注入:依賴注入模式
[ASP.NET Core 3框架揭秘] 依賴注入:一個迷你版DI框架