1. 程式人生 > 實用技巧 >控制反轉、依賴注入(IOC、DI)

控制反轉、依賴注入(IOC、DI)

  • IOC: Inversion Of Control 控制反轉
  • DI: Dependency Injection 依賴注入

1.控制反轉 Inversion Of Control 的前世今生

1.1 IOC理論產生的背景

討論控制反轉之前,先看看軟體系統提出控制反轉的前世今生。
一個完整精密的軟體系統,元件之間就像齒輪,協同工作,相互耦合。

  • 一個零件不正常,整個系統就崩潰了。
  • 系統物件之間耦合關係無法避免,在專案規模和複雜度變大的情況下,管理類之間的依賴關係將會很複雜。
  • 物件之間耦合度很高的系統,架構師和開發人員對於系統的修改,必然會出現牽一髮而動全身的情形。
  • 物件之間耦合性依賴,單元測試很複雜。
1.2 IOC理論

軟體專家為此提出IOC理論,用來實現物件之間的解耦。
再來看看,控制反轉(IOC)到底為什麼要起這麼個名字?我們來對比一下:

  • 軟體系統在沒有引入IOC容器之前,物件A依賴於物件B,那麼物件A在初始化或者執行到某一點的時候,自己必須主動去建立物件B或者使用已經建立的物件B。無論是建立還是使用物件B,控制權都在自己手上。
  • 軟體系統在引入IOC容器之後,這種情形就完全改變了,由於IOC容器的加入,物件A與物件B之間失去了直接聯絡,所以,當物件A執行到需要物件B的時候,IOC容器會主動建立一個物件B注入到物件A需要的地方。

通過前後對比,我們不難看出:
物件A獲得依賴物件B的過程,由主動變為了被動行為,控制權顛倒過來,這就是“控制反轉”的由來。

1.3 控制反轉 和 依賴注入

有些人會把控制反轉和依賴注入等同,實際上有本質區別:
控制反轉是一種思想;依賴注入是一種設計模式。
依賴注入是實現控制反轉的一種方式,但是控制反轉還有其他實現方式,例如說ServiceLocator(服務定位器、依賴查詢),所以不能將控制反轉和依賴注入等同。

2 依賴注入 Dependency Injection

依賴注入:容器全權負責元件的裝配,它會把符合依賴關係的物件通過屬性或者建構函式傳遞給需要的物件。

符合依賴倒置原則,高層模組不應該依賴低層模組,兩者都應該依賴其抽象

2.1 ASP.NET Core依賴注入

使用方式大體類似:

  • 定義依賴實現的介面或者抽象類
  • 在服務容器中註冊元件依賴 :IServiceProvider
  • 在建構函式中注入服務, 框架會負責建立和銷燬例項
 1 // 編寫元件和服務
 2 public interface IMyDependency
 3 {
 4     string WriteMessage(string message);
 5 }
 6 ---
 7 public class MyDependency : IMyDependency
 8 {
 9     public string WriteMessage(string message)
10     {
11        return $"MyDependency.WriteMessage Message: {message}";
12     }
13 }
14 // 註冊元件和依賴,下面註冊的`IMyDependency`在一個web請求中有效
15 public void ConfigureServices(IServiceCollection services)
16 {
17     services.AddScoped<IMyDependency, MyDependency>();
18     services.AddRazorPages();
19 }
20 ---
21 // 在建構函式注入元件
22 public class HomeController: AbpController
23 {
24     private readonly IMyDependency _dep;
25     public HomeController(IMyDependency dep)
26     {
27        _dep = dep;
28     }
29 
30   public IActionResult Index()
31   {
32     var content =  _dep.WriteMessage($"The Reflection instance is {_dep.GetType().FullName} ");
33     return Content(content);
34   }
35 }

在請求某個服務時,框架會完整解析出這個物件的依賴樹和作用範圍。

上面的示例程式碼形成 req--->HomeController--->IMyDependency依賴樹。

IMyDependency在每個web請求範圍內使用同一服務例項。

輸出:MyDependency.WriteMessage Message: The Reflection instance is TestDI.MyDependency

2.2 物件生命週期

根據現實需要,前人從使用場景中總結出三種服務生命週期。
ASP.NET Core提供了一個列舉ServiceLifetime

-----------
Singleton 單例 服務容器首次請求會建立,後續都使用同一例項 AddSingleton
Scoped 特定範圍 在一個請求(連線)週期內使用一個示例 AddScoped
Transient 瞬時 服務容器每次請求,都會建立一個例項 AddTransient

對於Scoped Service的理解:

在webapp:scoped service 會在請求結束時被銷燬;
在EFCore:使用AddDbContext預設註冊的是特定範圍的DbContext,這意味在我們可以在一次sql連線內,使用同一個DbContext例項進行多次DB操作。

2.3 依賴注入實現原理

結合理論、使用方式 猜測依賴注入的原理:
實現DI,核心在於依賴注入容器IContainer,該容器具有以下功能
①.(容器)儲存可用服務的集合
// 要用的特定物件、特定類、介面服務

②.(註冊)提供一種方式將各種部件與他們依賴的服務繫結到一起;
// Add...函式或containerBuilder.Register函式

③.(解析點)為應用程式提供一種方式來請求已配置的物件:建構函式注入、屬性注入.

執行時,框架會一層層通過反射構造例項,最終得到完整物件。

3.原始碼導航

利用反射產生物件是依賴注入的核心過程,這也是面試造航母時經常問到的。

.NETSystem.ReflectionSystem.Type名稱空間中的類可以獲取可裝配元件、類、介面的資訊,並提供了在執行時建立例項,呼叫動態例項方法、獲取動態例項的能力。

實際上,我們可以在依賴樹的尾部物件的建構函式手動丟擲異常,異常的呼叫棧就是一個天然的原始碼導航。

於是我在上面示例程式碼的request----> HomeController--->MyDependency MyDependency建構函式中新增異常程式碼:

1    public MyDependency()
2     {
3        throw new Exception("exception content!");
4     }

結果如下圖:

從Github Dependency Injection 庫進入System.Reflection的呼叫分界線程式碼:

 1 protected override object VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
 2 {
 3     object[] parameterValues;
 4     if (constructorCallSite.ParameterCallSites.Length == 0)
 5     {
 6          parameterValues = Array.Empty<object>();
 7     }
 8     else
 9     {
10        parameterValues = new object[constructorCallSite.ParameterCallSites.Length];
11        for (var index = 0; index < parameterValues.Length; index++)
12       {
13          parameterValues[index] = VisitCallSite(constructorCallSite.ParameterCallSites[index], context);
14        }
15     }
16 
17     try
18     {
19        return constructorCallSite.ConstructorInfo.Invoke(parameterValues);
20     }
21     catch (Exception ex) when (ex.InnerException != null)
22     {
23        ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
24       // The above line will always throw, but the compiler requires we throw explicitly.
25       throw;
26      }
27 }

黃色背景行就是.NET反射特性的體現:
對型別資訊(建構函式、引數)使用Invoke方法產生物件。

乾貨旁白

  • 控制反轉是一種在軟體工程中解耦合的思想,呼叫方依賴介面或抽象類,減少了耦合,控制權交給了服務容器,由容器維護註冊項,並將具體的實現動態注入到呼叫方。
  • 有些人會把控制反轉和依賴注入等同,實際上有本質區別:
  • 控制反轉是一種思想;
  • 依賴注入是一種設計模式。
  • 依賴注入是實現控制反轉的一種方式,但是控制反轉還有其他實現方式,例如說ServiceLocator,所以不能將控制反轉和依賴注入等同。
  • 在執行時,框架會解析依賴樹、依賴圖,通過反射在執行期生成物件。