DI的一些事
傳送門馬丁大叔的文章
什么是依賴注入(DI: Dependency Injection)?
依賴注入(DI)是一種面向對象的軟件設計模式,主要是幫助開發人員開發出松耦合的應用程序。同時呢,讓應用更容易進行單元測試和維護。
DI其實就是用一個注入器類為一個對象提供其依賴的一個過程!如何更好的理解呢?下面就舉個列子解釋下!
比如 class Client,它要使用服務class Service的提供的功能,這個時候就說Service是Client的依賴,程序實現如下:
var s = new Service();
var c = new Client(s);
很明顯我們還要承擔創建Service的對象的職責,程序出現了強耦合問題,后面如果需求變化,我們要替換掉Service,那我們就要修改這邊的代碼,這樣的程序很面明,擴展性,靈活性比較差了!
引入DI之后呢,我們應該還有一個注入器類,假設是 class Injector 。為了更好的解釋DI的好處,上面的代碼我們重新設定為 class Client 依賴 接口IService , class Service 實現了IService ,這個時候我們的程序主流程不需要關注如何創建的Service,可以把這部分的職責委托給Injector,我們只要告訴Injector,我需要IService,請提供給我,程序實現如下:
var s = Injector.Get(typeof(IService));
var c = new Client(s);
這樣的好處就很明顯了,我們只關注自己的核心業務職責,對應依賴如何創建的,具體是什么類實現的,都不用自己管了,權力交給注入器就可以了!
划重點:其實上面這個過程大家應該發現了我們把本來自己的一部分控制權,轉交給了注入器去做,這個就是我們經常說的IOC(Inversion of Control,控制反轉)。DI其實就是IOC計原則的一種實現。還有我們平常說的觀察者默認,其實也是IOC的一種實現,核心就是把部分職責(非核心職責)轉交出去,從而去構建出一種松耦合的應用!
什么是依賴注入容器(DI Container)?
DI Container ,也可以叫 IOC Container,其實是一個框架(Framework),它提供了一整套的DI解決方案,它負責創建依賴,然后自動把依賴注入到需要它們的其他對象里面,同時還負責管理依賴的生命周期!一些強大的第三方容器還提供各種各樣的功能,使我們更加愉快的擼代碼!
常用的第三方DI Container:
-
Spring.NET
-
Autofac
-
Unity
-
Ninject
ASP .NET Core中的DI
在ASP.NET Core中,把依賴統一稱作服務(services),所以DI Container就也被稱為Service Container,Asp.NET Core提供了一個簡單的內置容器 IServiceProvider ,它默認支持構造器注入 constructor injection,滿足我們大多數的功能需求!
服務主要分為兩類:
-
Framework Services:框架服務,由ASP.NET Core框架提供,例如
IApplicationBuilder、IHostingEnvironment... ,詳情見https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1#framework-provided-services 。 -
Appliction Services:應用服務,由我們根據實際業務創建。
如果要通過DI容器自動實現注入我們的服務,我們必須要先在容器中登記服務(只需要注冊應用服務,框架服務已經被ASP.NET Core框架注入了)。
注冊服務(Registering Services)
假設我們有一個ILog接口和它的一個實現類,我們要把它注入到DI容器里面,然后在應用中使用它。
public interface ILog
{
void Info(string msg);
void Error(string err);
}
public class ConsoleLogger:ILog
{
public void Info(string msg)
{
Console.WriteLine(msg);
}
public void Error(string err)
{
Console.WriteLine(err);
}
}
然后在Startup類的ConfigureServices()方法中注冊上面的服務,ConfigureServices()有一個IServiceCollection參數,就是用它來注冊應用服務。
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.Add(new ServiceDescriptor(typeof(ILog), typeof(ConsoleLogger), ServiceLifetime.Singleton));
}
}
類ServiceDescriptor用來描述服務的類型、服務的具體實現已經服務的一個生命周期(Service Lifetime),上面我們指定了服務ILog的實現是ConsoleLogger,且是一個單例(Singleton)。
通常情況我們都是使用IServiceCollection擴展方法來注冊服務
services.AddSingleton<ILog, ConsoleLogger>();//注冊為單例
services.TryAddSingleton<ILog, ConsoleLogger>();
構造函數注入(Contractor Injection)
一旦我們注冊了函數,當應用類的構造函數包含了需要依賴的服務,DI容器就自動幫我們注入依賴。
public class HomeController : Controller
{
ILog _log;
public HomeController(ILog log)
{
_log = log;
}
public IActionResult Index()
{
_log.Info("Executing /home/index");
return View();
}
}
控制器需要ILog服務,只需要在構造函數的參數中包含ILog類型即可,我們不需做其他任何事,DI容器自動給我們創建ILog的實例,並根據注入時指定的生命周期,在切當是時機銷毀(Dispose)這個實例。
Action方法注入(Method Injection)
有時候,我們只需要在某一個方法中需要這個服務,這時,我們可以給方案的參數標記上[FromServices] 這個特性,容器就能自動為我們注入這個依賴服務的實例了
public IActionResult Index([FromServices] ILog log)
{
log.Info("Index method executing");
return View();
}
屬性注入(Property Injection)
ASP.NET Core自帶這個容器不支持,需要使用第三方容器,例如Autofac 。
服務的生命周期(Service Lifetime)
DI容器負責管理已注冊服務的生命周期,它根據指定的生命周期自動銷毀服務實例。ASP.NET Core 的服務可以配置以下三種生命周期形式:
-
Transient(瞬態)
每次從DI容器中解析(獲取)服務時,DI容器都是返回一個新的服務實例。
//原始方法 services.Add(new ServiceDescriptor(typeof(ILog), typeof(ConsoleLogger), ServiceLifetime.Transient)); //擴展方法 services.AddTransient<ILog, ConsoleLogger>(); -
Scoped(作用域)
每一個作用域范圍內(例如每一個HTTP 請求)從DI容器中解析出來的實例都是同一個
//原始方法 services.Add(new ServiceDescriptor(typeof(ILog), typeof(ConsoleLogger), ServiceLifetime.Scoped)); //擴展方法 services.AddScoped<ILog, ConsoleLogger>(); -
Singleton(單例)
只在第一次請求是創建,之后所有請求都共享同一個服務實例,直到應用程序的生命周結束。所以在ASP.NET Core應用中,沒有必須手動去創建一個單例,通過注冊一個單例服務到容器中即可。
//原始方法 services.Add(new ServiceDescriptor(typeof(ILog), new ConsoleLogger())); //擴展方法 services.AddSingleton<ILog, ConsoleLogger>();
DI使用經驗的一些總結
泛型如何注冊?
通過開放式泛型(Open Generics)注冊服務。假設上面的類型修改成 ILog<T> 和
ConsoleLogger<T> ,那么我們按照下面的方式注冊即可
services.AddScoped(typeof(ILog<>), typeof(ConsoleLogger<>));
相同的接口類型注入兩個實現類型會怎樣?
如果我們在注冊服務時,相同類型注冊多次並不會報錯,但在解析時,返回的是最后一次注冊的類型的實例。
public interface ILog
{
void Info(string msg);
}
public class Logger1 : ILog
{
public void Info(string msg)
{
}
}
public class Logger2 : ILog
{
public void Info(string msg)
{
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ILog, Logger1>();
services.AddTransient<ILog, Logger2>();
}
}
public class HomeController : Controller
{
ILog _log;
public HomeController(ILog log)
{
_log = log;//_log is Logger2
}
}
為了避免我們多次注冊,導致具體實現被覆蓋的問題,所以我們一般都是使用 TryAddTransient,這個方法注冊時,檢測到相同類型已經被注冊過,就不會在進行注冊
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.TryAddTransient<ILog, Logger1>();
services.TryAddTransient<ILog, Logger2>();
}
}
public class HomeController : Controller
{
ILog _log;
public HomeController(ILog log)
{
_log = log;//_log is Logger1
}
}
自己想要手動從容器中獲取服務對象,怎么做?
ASP.NET Core中的DI Container是IServiceProvider,只要獲取這個對象,然后調用 GetService 這個方法即可。
-
如果在Controller中
HttpContext的屬性
RequestServices就是IServiceProvider類型,所以我們可以按照下面的方法:public IActionResult Index() { var services = this.HttpContext.RequestServices; var log = (ILog)services.GetService(typeof(ILog)); log.Info("Index method executing"); return View(); } -
如果在應用中
在應用中時,我們可以通過構造函數注入
IServiceProviderpublic class MyAppService { private IServiceProvider _services; public MyAppService(IServiceProvider services) { _services=services; } public void Test() { //原始方法 var log = (ILog)_services.GetService(typeof(ILog)); //通過擴展方法,需要nuget添加Microsoft.Extensions.DependencyInjection.Abstractions 這個引用 var log = _services.GetService<ILog>(); } }
結語
ASP.NET Core已經很強大了,提供了很多實用功能,希望.Net的戰友們能在基本知識儲備的前提下,多多發掘總結出ASP.NET Core開發的最佳實踐,為推動.NET Core生態建設貢獻一份自己的力量!💪
