--- tags: 軟體工程, Dependency Injection description: 依賴注入(Dependency Injection)課程 --- # 依賴注入(Dependency Injection)分享 DI在目前許多軟體架構中是不可或缺的設計架構功能之一,此章節會闡述DI精神及介紹.Net的DI使用。 ## A、依賴注入(Dependency Injection) 先來講依賴跟注入的概念 ### a.Dependency 依賴如字面上所述,在軟體中,我們很常遇到物件彼此相依的狀況。如下圖,在做計算時需做Printer輸出資訊,因此Calculator物件會相依Printer物件,則在Instance Calculator物件時則就需要同步Instance Printer物件並帶入Calculator物件中。 ![](https://i.imgur.com/xG5XLon.png) ```csharp= public class Calculator { public Printer Printer {get;private set} public Add(int a, int b){ var sum = a+b; Printer.ConsoleOut(sum); } public Calculator(Printer printer){ Printer = printer; } } public class Printer { public void ConsoleOut(string txt) { Console.WriteLine(txt); } } static class Program { static void Main() { var printer = new Printer(); var calculator = new Calculator(printer); calculator.Add(1+1); } } ``` 這例子為Calculator依賴Printer,所以控制權不是在Printer身上。什麼意思? 簡單來說當Printer有異動時,會有一定程度是影響到Calculator的。 對於物件依賴這件事情我在網上看到一個例子描述還蠻貼切的,今天有一名8+9在吸食毒品如下。8+9 以為自己吸食毒品只是滿足自己的快感,他還以為他自己的意志行為是被自己所掌控 錯!!! 其實8+9這時候所有的行為已經被毒品所控制,再也離不開毒品了。 ![](https://i.imgur.com/x1D8TEl.png) ### b.Dependency With Interface 那我們該如何解除依賴關係? 一般來說,在實作方法時,我們會讓物件去相依介面,讓物件彼此因介面而解偶,如下圖 ![](https://i.imgur.com/B6usNkb.png) ```csharp= public class Calculator { public IPrinter Printer {get;private set} public Add(int a, int b){ var sum = a+b; Printer.ConsoleOut(sum); } public Calculator(IPrinter printer){ Printer = printer; } } public class Printer:IPrinter { public void ConsoleOut(string txt) { Console.WriteLine(txt); } } public interface IPrinter{ void ConsoleOut(string txt); } static class Program { static void Main() { IPrinter printer = new Printer(); var calculator = new Calculator(printer); calculator.Add(1+1); } } ``` 因相依介面,若我們要抽換功能,例如原本的Console輸出改成輸出至記事本。只需要在新增一個記事本輸出方法,替換掉原本的28行的printer即可。而這狀況我們稱為Dependency Inversion Principle(DIP),依賴倒轉原則。簡單來說就是解除高階模組和低階模組的依賴關係。 > 高低階模組定義 高階模組指的是呼叫者(Caller)-Calculator 低階模組就是被呼叫者(Callee)-Printer 我們再回到8+9例子,今天想要讓8+9改過向上,戒毒成為奮發向上的好青年。因為現在8+9直接依賴著毒品,我們讓8+9改成依賴藥品這個介面。 ![](https://i.imgur.com/NhQhLBP.png) 現在我們成功讓8+9依賴藥品而不是毒品。然後再將做一個健康食品的類別實作藥品,然後偷偷把毒品換成健康食品。這樣子8+9以為自己吃的藥品是毒品,實際上卻是每天吃健康食品,於是8+9頭腦變好了 走路也不晃了 考試也考100分成為國家的棟樑。 ### c.Injection 接著講注入,上述Calculator例子注入方式稱為建構子注入,其實注入平常我們就有在使用。一般人比較不會注意到這些行為其實就是一種注入。常見的注入模式有三種 - 建構子注入(Constructor Injection) - 屬性注入(Property Injection) - 方法注入(Method Injection) ##### 建構子注入(Constructor Injection) ```java= // 汽車的抽象介面 public interface ICar { // 行駛汽車 void run(); } // 司機的抽象介面 public interface IDriver { // 司機駕駛汽車 void drive(); } // 實作司機的抽象類別 public class Driver implements IDriver { private ICar car; // 建構子方式注入抽象類別 public Driver(ICar car){ this.car = car; } @Override public void drive() { car.run(); } } ``` ##### 屬性注入(Property Injection) ```java= // 汽車的抽象介面 public interface ICar { // 行駛汽車 void run(); } // 司機的抽象介面 public interface IDriver { // 司機駕駛汽車 void drive(); } // 實作司機的抽象類別 public class Driver implements IDriver { private ICar car; // 使用setter來傳遞物件 (c# get set) public setCar(ICar car){ this.car = car; } @Override public void drive() { car.run(); } } ``` ##### 方法注入(Property Injection) ```java= // 汽車的抽象介面 public interface ICar { // 行駛汽車 void run(); } // 司機的抽象介面 public interface IDriver { // 司機駕駛汽車 void drive(ICar car); } // 實作司機的抽象類別 public class Driver implements IDriver { // 使用介面來傳遞物件 @Override public void drive(ICar car) { car.run(); } } ``` > 在了解Dependency與Injection後,應該就可以理解相依注入動作就在於在Instance物件後放置對應的相應物件中 ## B、容器概念 上述DI概念講完後,接著我們要述說容器的概念。A章節提到關於相依的問題,雖然我們可以透過介面去作解偶。但一般在大型專案物件複雜度及數量一定會是更複雜的狀態。因此有了容器的機制設計產生。 容器的概念在於把物件全都收到一個盒子中,當要使用物件時再去盒子拿資料。有點像是打高爾夫球人員要打高爾夫時都會準備一個球袋把要用的球桿放進球袋中,並帶一個球僮陪伴在旁。當要使用哪一支高爾夫球桿時直接告訴球童即可。 ![](https://i.imgur.com/NMHkG5U.png) ### a.窮人與有錢人的依賴注入方式 上述講解完後,相信目前對相依注入及容器有一定的概念。而注入部分一般分成窮人注入與有錢人注入。 #### 窮人的注入方式 我們一般稱為Poor DI(Dependency Injection),由使用者手動創建以new物件的方式,各自注入,除了麻煩外而且會有傳遞注入等等的問題。 #### 有錢人的注入方式 有錢人的注入方式,我們一般都會使用DI容器來完成,由框架或類別庫撰寫的容器,提供物件給予使用者使用,猶如上述所講的球童跟高爾夫球員關係一樣。使用者只需要在撰寫程式前設定好物件後,就不需要在手動注入。 ## C、Ioc(Inversion of Control)控制反轉 窮人跟有錢人,我們當然選擇當有錢人!(使用DI容器)。若我們使用DI容器去管理我們物件時,此時我們即可達到控制反轉的設計。 簡單來說A物件程式內部需要使用B物件,程序需自行在A物件去new B物件。由程序主動去控制,有了容器,這一切交給容器去控制(新增物件流程)即可。 如果說DIP 是解偶物件之間的依賴,IOC 就是對物件依賴流程控制的反轉。以常見的web框架而言,將監聽http 請求,解析請求等,包裝進框架,一般使用者並不用主動去處理解析請求,處理http請求的流程,由主動處理轉交給了框架,因此,所以框架其實也是一種IoC的設計。 > 你可以將IOC是一種設計模式,將流程控制重定向到**外部處理程序**或**控制器**來提供控制結果,而不是透過控制項(大多情境就是程序)來直接得到結果 ## D、C# DI 框架 [[範例請點我]](https://github.com/spyua/DIWindowsFormSample.git) 上述聊完後,相信對於DI、容器、DIP與IOC有一定認知。接著就需要進入實作部分。在.NET中最常見的DI有兩套如下 - Microsoft.Extensions.DependencyInjection (ASP.NET DI) - Autofac 這兩套DI工具,不管使用哪一套,基本上我們只著重在兩件事情 1. DI注入方法 2. DI注入服務生命週期型態 上述提到注入三種方式(建構子、屬性與方法),Autofac都有提供,而ASP.NET DI的目前只有建構子注入方式。網上大多是Web範例,在此使用這兩套DI工具使用在DeskTop Windows From。 ### Microsoft.Extensions.DependencyInjection 範例DI設定在DIServiceConfigure.cs #### 注入服務生命週期型態 服務生命週期指:透過 DI 取得某個元件時,是每次要求得建立一顆新物件,還是從頭到尾共用一個 Instance (執行個體-物件)。 - 1.AddSingleton (單一性) - 2.AddTransient (暫時性) - 3.AddScoped (範圍性) #### 1. AddSingleton (單一性) 整個程序只建立一個 Instance,任何時候都共用它。簡單來說就是程序中不同流程使用此物件時,他都為同一份物件,有點像是Static,差在於使用DI可做物件設計及抽換。 下圖為Asp.Net DI生命週期圖示,物件相依關係由左至右。 ![](https://i.imgur.com/8tt7aTj.png) 而在範例模擬中設計,物件對應可視為 - Rqeuest : Button_Singleton_Click 按下 - 第一個圈 : Call LogController - 第二個圈 : Call LogController2 - Instance : SingletonLogRepository Repository注入部分我們使用AddSingleton,而物件在Instance時,我們會建置Guid去觀察他是否為同一個物件。 ```csharp= // 系統單一物件定義使用,或沒有異部與大量Request問題,可直接用Singleton collection.AddSingleton<ISingletonLogRepository, LogRepository>(); ``` 當我們按下Button_Singleton_Click時,會去呼叫LogController及LogController2裡的SingletonLogRepository物件。並印出Guid。 ```csharp= private void Button_Singleton_Click(object sender, EventArgs e) { //var provider = DIServiceConfigure.GetProvider(); //var logController = provider.GetRequiredService<ILogController>(); var log = "Log1(Singleton):"+"UUID-"+LogController.OperationId("Singleton"); var log2 = "Log2(Singleton):" + "UUID-" + LogController2.OperationId("Singleton"); richTextBox_Info.AppendText(log+"\n"); richTextBox_Info.AppendText(log2 + "\n"); richTextBox_Info.AppendText(LogController.QueryLogCount()); } ``` 此時我們可以看到兩個印出來的UUID會完全相同。代表在SingletonLogRepository在Controller與Controller2中為同一個物件。 ![](https://i.imgur.com/YWR1Vvn.png) #### 2. AddTransient (暫時性) 定義上為程序中每次要求物件(包含物件要其他相依物件)時就建立一個新的,永不共用。 下圖為 Asp.Net DI生命週期圖示,物件相依關係由左至右。 ![](https://i.imgur.com/JJTBjyd.png) 而在範例模擬中設計,物件對應可視為 - Rqeuest : Button_Transient_Click 按下 - 第一個圈 : Call LogController - 第二個圈 : Call LogController2 - Instance : TransientLogRepository Repository注入部分我們使用AddTransient,而物件在Instance時,我們會建置Guid去觀察他是否為同一個物件。 ```csharp= // 異步,且須大量Request的建議使用Transient (WundowsForm簡易Sample較難模擬) collection.AddTransient<ITransientLogRepository, LogRepository>(); ``` 當我們按下Button_Transient_Click時,會去呼叫LogController及LogController2裡的SingletonLogRepository物件。並印出Guid。 ```csharp= private void Button_Transient_Click(object sender, EventArgs e) { // 模擬多次Request //var provider = DIServiceConfigure.GetProvider(); //var logController = provider.GetRequiredService<LogController>(); //var log = "Log1(Transient):" + "UUID-" + logController.GUID.ToString(); //richTextBox_Info.AppendText(log + "\n"); // 單次Request var log = "Log1(Transient):" + "UUID-" + LogController.OperationId("Transient"); var log2 = "Log2(Transient):" + "UUID-" + LogController2.OperationId("Transient"); richTextBox_Info.AppendText(log + "\n"); richTextBox_Info.AppendText(log2 + "\n"); //richTextBox_Info.AppendText(LogController.QueryLogCount()); } ``` 此時我們可以看到兩個印出來的UUID會不相同。代表在TransientLogRepository在Controller與Controller2中為不同物件。 ![](https://i.imgur.com/BJzzoMB.png) 上述模擬範例其實只模擬一個Request1的狀況。根據定義,程序中每次要求元件時就建立一個新的物件,對此我們已再按多次Button來做模擬。要模擬這狀況,就需要再跟Container拿取Controller物件,在範例中Controller與Controller2都是注入AddTransient,我們將模擬多次Request區塊註解打開,並註解單次Request程式碼 ```csharp= private void Button_Transient_Click(object sender, EventArgs e) { // 模擬多次Request var provider = DIServiceConfigure.GetProvider(); var logController = provider.GetRequiredService<LogController>(); var log = "Log1(Transient):" + "UUID-" + logController.GUID.ToString(); richTextBox_Info.AppendText(log + "\n"); // 模擬單次Request //var log = "Log1(Transient):" + "UUID-" + LogController.OperationId("Transient"); //var log2 = "Log2(Transient):" + "UUID-" + LogController2.OperationId("Transient"); //richTextBox_Info.AppendText(log + "\n"); //richTextBox_Info.AppendText(log2 + "\n"); //richTextBox_Info.AppendText(LogController.QueryLogCount()); } ``` 此時我們可以看到每次的UUID就會不相同,代表在Controller在每次跟Container拿取時都為不同物件。相對的,注入其他物件時也是同一個狀況。 ![](https://i.imgur.com/qwudf0O.png) #### 3. AddScope (範圍性) Scope我覺得是最難了解的,直接看 Asp.Net DI生命週期圖示,物件相依一樣關係由左至右。如圖所示,程序中要求物件(包含物件要其他相依物件)時,這個Flow處理過程中用到的物件則為同一個物件。而第二次再次要物件時,則會跟第一次的物件是不一樣的物件。 ![](https://i.imgur.com/z8DbOLj.png) 我們已Call Dialog方式去模擬多次Request,程式碼這邊不多做解釋,結果與物件對照如下圖,觀察Instance部分物件分別為 - SingletonLogRepository - TransientLogRepository - ScopedLogRepository 可以看到ScopedLogRepository,在同一次的Request Flow,LogController與LogService的LogRepository UUID為一樣的,但在第二次打開Dialog時,則UUID會與第一次的不同。這點會跟Singleton有很大的不同。 在此我們也可以看到Transaction則是在每個物件中,及每次Request都為不同UUID。 ![](https://i.imgur.com/Val3RkA.png) 其實Scoped跟Transaction之間有時候蠻容易混淆的。對我來說比較好懂得在於Scoped在跟Container要物件時,若前一個物件還未dispose(生命週期還未結束),則都是同一個物件。物件結束生命週期後在新的Request後才會在新增新的物件。而Transaction就無生命週期概念,在每一次要求物件時,都會New一個新的給他,有點像平常我們想用物件就New Instance的概念。 #### 生命週期小結論 上述描述完後,沒意外應該會對注入的生命週期會有所了解。而在Web的世界中,這三種注入方式會很常被使用到。在每一次Http Request情境,Request具有連線即結束,在這狀況下的連線相關物件基本上就會使用Scoped,因此在Web情境,大多數物件都會使用Scoped注入。 但WindowsForm App下,其實我們最常使用的會是Singleton居多,其次是Transaction,Scoped狀況就比較少,因為WindowsForm一開始畫面就那些就載入到 memory中,除非我們將畫面dispose掉,然後再重新開啟畫面,此時就會建議使用Scoped注入。 Transaction使用場景比較會偏向用後就直接dispose,不過這種應用場景,我們很常直接手動去new物件.實際架構應用上目前我也沒太多經驗。 #### 注入抽換 生命週期講完後,接著就是闡述關於抽換實體物件。範例中我們使用IPrinter注入抽換PrinterMethodA與PrinterMethodB。 在一開始我們IPrinter注入為PrinterMethodA,在按下button_Printer_Click按鈕時,Console則會印出MethodA Print:Prionter Out ![](https://i.imgur.com/z4sZxFK.png) 此時我們將PrinterMethodA改成PrinterMethodB ```csharp= collection.AddTransient<IPrinter, PrinterMethodB>(); ``` 此時在按下button_Printer_Click按鈕時,Console則會印出MethodB Print:Prionter Out ![](https://i.imgur.com/Z8MLaZM.png) 另外我們也可以注入IPrinter List,只要注入多Concreate實體 ```csharp= collection.AddTransient<IPrinter, PrinterMethodA>(); collection.AddTransient<IPrinter, PrinterMethodB>(); ``` 就可注入多PrinterMethod方法 ```csharp= private LogController LogController; private LogController2 LogController2; // 多重注入 private IEnumerable<IPrinter> Printer; public Form1(IEnumerable<IPrinter> printer, LogController logController, LogController2 logController2) { InitializeComponent(); LogController = logController; LogController2 = logController2; // 多重注入 Printer = printer; } ``` ![](https://i.imgur.com/sAjdrOx.png) 我們只須改Container設定,主程式都不用更動,即可抽換程式中所有物件用到IPrinter的地方。 ### AutoFac 範例DI設定在AutofacConfig.cs AutoFac與 ASP.NET DI 注入生命週期概念其實大同小異,下圖為對應表[[出處]](https://devblogs.microsoft.com/cesardelatorre/comparing-asp-net-core-ioc-service-life-times-and-autofac-ioc-instance-scopes/) ![](https://i.imgur.com/w4Im0QM.png) 根據對應表,在範例程式要換成Autofac範例,只需將Bootstrapper與Form.cs裡Button_Scoped_Click的Net DI程式碼註解掉,並解開Autofac註解相關程式碼。即可觀察注入物件生命週期的不同。