Visual Studio 在 ASP.NET Core Web Application 的 template 中,預設已經可使用 DI,若要在 console app 也使用 DI,需要另外設定。
Version
Visual Studio 2017 15.4.5
.NET Core 2.0
Microsoft.Extensions.DependencyInjection 2.0.0
安裝 NuGet 套件
GUI 安裝
- 在 project 選擇
Dependencies
- 按滑鼠右鍵選擇
Manage NuGet Packages...
DI 主要是在 Microsoft.Extensions.DependencyInjection
套件裡,console app 預設沒有安裝,需要手動自行安裝。
- 選擇
Browse
- 輸入
DependencyInjection
- 選擇
Microsoft.Extensions.DependencyInjection
套件 - 按
Install
安裝套件
安裝完成後,在 Dependencies
下的 NuGet
會看到 Microsoft.Extensions.DependencyInjection
套件。
指令安裝
Tools -> NuGet Package Manager -> Package Manager Console
啟動 package manager console
PM> Install-Package Microsoft.Extensions.DependencyInjection
使用 Install-Package
指令安裝 Microsoft.Extensions.DependencyInjection
套件。
安裝完成後,在 Dependencies
下的 NuGet
會看到 Microsoft.Extensions.DependencyInjection
套件。
Architecture
Service1
使用 delagation 手法,將 Execute()
轉發給 Service2.SayHello()
,其中 Service2
是 Service1
的 dependency。
傳統寫法我們會在 Service1
內去 new
Service2
,也就是高階模組 Service1
直接相依於低階模組 Service2
,這就違反了 依賴反轉原則
。
依賴反轉原則
DIP : Dependency Inversion Principle
高階模組不應該相依於低階模組;低階模組應該相依於高階模組所定義的抽象
白話 : 原本是高層依賴於底層,現在是反過來底層依賴於高層所定義的 interface
所以高階模組 Program
與 Service1
定義出 IService1
與 IService2
interface,由低階模組 Service1
與 Service2
去實現,此時高階模組 Program
與 Service1
不再直接相依於 Service1
與 Service2
,而是反過來相依於 Program
與 Service1
所定義的 IService1
與 IService2
,符合 依賴反轉原則
。
因此我們改用 依賴注入
方式,由 Service1
的 constructor 將符合 IService2
interface 的 Service2
注入進去。
而 DI Container 就是負責幫我們 new
物件,然後由 constructor 注入。
Implementation
IService2
IService2.cs
interface IService2 { void SayHello(); }
因為 Service1
為 Service2
的高階模組,由 Service1
定義出 IService2
,由 Service2
所依賴。
Service2
Service2.cs
class Service2 : IService2 { public void SayHello() { Console.WriteLine("Hello World"); } }
低階模組 Service2
實現 (依賴) IService2
。
IService1
IService1.cs
interface IService1 { void Execute(); }
因為 Program
為 Service1
的高階模組,由 Program
定義出 IService1
,由 Service1
所依賴。
Service1
Service1.cs
class Service1 : IService1 { private readonly IService2 service2; public Service1(IService2 service2) { this.service2 = service2; } public void Execute() { this.service2.SayHello(); } }
低階模組 Service1
實現 (依賴) IService1
。
第 3 行
private readonly IService2 service2; public Service1(IService2 service2) { this.service2 = service2; }
傳統我們都會在 Service1
的 constructor 去 new Service2
,
private readonly IService2 service2; public Service1() { this.service2 = new Service2(); }
這樣 Service1
就直接耦合了 Service2
,現在我們改用在 constructor 依賴注入
IService2
型別的 Service2
。
這樣做有幾個優點 :
- 由原本相依於
Service2
,改相依於IService2
interface,符合依賴反轉原則
- 原本直接在 constructor 去
new
,高階模組無法抽換低階模組,現在高階模組可透過 constructor 直接換掉低階模組,原來的Service1
程式碼不用變動,符合開放封閉原則
- 單元測試時,可直接透過 constructor 注入 mock 物件隔離測試
OOP 心法
若使用 DI 寫法,則不須再使用 new
,讓程式碼更為乾淨,類似 static class 只要一行程式碼就可解決
Program
Program.cs
class Program { static void Main(string[] args) { var services = new ServiceCollection(); services.AddTransient<IService1, Service1>(); services.AddTransient<IService2, Service2>(); var serviceProvider = services.BuildServiceProvider(); IService1 service1 = serviceProvider.GetService<IService1>(); service1.Execute(); } }
Service1
已經用了 依賴注入
,讓我們可以在 constructor 注入相依的 Service2
,傳統我們會這樣建立 Service1
:
class Program { static void Main(string[] args) { var service1 = new Service1(new Service2()); service1.Execute(); } }
直接使用 new
,並在其 constructor 傳進 new Service2()
。
但實務上 service 與 service 間的關係會更複雜,因此 constructor 寫法不會這麼簡單。
而 DI container 就是幫我們不用在 constructor 寫複雜的 new
。
第 5 行
var services = new ServiceCollection(); services.AddTransient<IService1, Service1>(); services.AddTransient<IService2, Service2>();
要讓 DI container 幫我們 new
,首先必須告訴 DI container :
當遇到 xxx interface 時,請幫我注入 yyy class
.NET 提供了 ServiceCollection
,由它負責蒐集所有 interface 與 class 的 mapping。
-
AddTrasient()
: 每次注入時,都重新new
一個新的 instance -
AddScoped()
: 每個 request 都重新new
一個新的instance -
AddSingleton()
: 程式啟動後會new
一個 instance,之後會重複使用,也就是運行期間只有一個 instance
泛型參數部份 :
- 第一個參數為 interface
- 第二個參數為 class
也就是告訴 ServiceCollection
,當 constructor 的型別為此 interface 時,請幫我 new 這個 class。
public static IServiceCollection AddTransient<TService, TImplementation>(this IServiceCollection services) where TService : class where TImplementation : class, TService;
其中 TImplementation
的 constraint 為 TService
,也就是若你這樣寫
services.AddTransient<IService1, Service2>();
因為 Service2
根本沒有實作 IService1
,編譯會錯誤。
OOP 心法
在使用泛型時,一定要搭配 constraint,才會發揮泛型的威力
第 9 行
var serviceProvider = services.BuildServiceProvider(); IService1 service1 = serviceProvider.GetService<IService1>();
DI container 要靠 service provider 來建立 object,所以下一步要靠 ServiceCollection.BuildServiceProvider()
來產生 ServiceProvider
。
之後就可用 ServiceProvider.GetService()
來建立 object,而不須使用 new Service1(new Service2())
。
GetService()
泛型參數要傳的是 interface,而不是 class,因為 constructor 宣告的是 interface 型別,不是 class 型別。
Q : 使用 service provider 建立 object,與自己 new
object,差別在哪裡 ?
A : 你只須用 service provider 建立最外層的 object 即可,剩下相依物件,DI container 會幫你建立;但若你使用 new
,怎每一層的相依物件都要自己處理。
11 行
service1.Execute();
Object 已經被 DI container 建立,就可以使用一般的方式使用 method。
Q : 依賴注入與 DI container 除了讓我們不用複雜的 new
,還有其他優點嗎 ?
class Program { static void Main(string[] args) { var service1 = new Service1(); service1.Execute(); } }
在沒有使用 依賴注入
與 DI container 之前, Program
只能去 new Service1()
,至於 Service1
用了哪些相依物件, Program
高階模組無法決定,也就是高階模組已經被低階模組綁死了,無從決定低階模組到底用了什麼相依物件。
class Program { static void Main(string[] args) { var services = new ServiceCollection(); services.AddTransient<IService1, Service1>(); services.AddTransient<IService2, Service2>(); var serviceProvider = services.BuildServiceProvider(); IService1 service1 = serviceProvider.GetService<IService1>(); service1.Execute(); } }
第 5 行
var services = new ServiceCollection(); services.AddTransient<IService1, Service1>(); services.AddTransient<IService2, Service2>();
高階模組 Program
可以決定低階模組 Service1
相依什麼物件,只要在 ServiceCollection
加以定義即可。
也就是高階模組不再相依於低階模組,而是高階模組可以自行定義低階模組的相依物件,符合 依賴反轉原則
。
Q : 高階模組能決定低階模組有什麼意義呢 ?
一旦高階模組可以決定低階模組,就可以在程式一開始 if else
就決定使用那些低階模組,決定之後,程式就可以很簡單的執行,不須再 if else
判斷。
但若高階模組無法決定低階模組,則在低階模組勢必增加很多 if else
判斷,判斷在什麼條件下,使用什麼低階模組,而且 if else
會分散在各低階模組,程式複雜度會提高很多。
Conclusion
- Console app 無法使用 DI container,須自行安裝
Microsoft.Extensions.DependencyInjection
,可使用 GUI 安裝,可以使用指令安裝 - DI 可以讓程式碼更乾淨,用起來類似 static 只要一行
- DI container 讓
依賴注入
更容易實現,也符合依賴反轉原則
。
Reference
John Wu , ASP.NET Core 教學 - Dependency Injection
Microsoft , Introduction to Dependency Injection in ASP.NET Core
Joonas W , ASP.NET Core Dependency Injection Deep Dive
ASP.NET Hacker , Using Dependency Injection in .NET Core Console Apps
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。