在我接觸IOC和DI 概念的時候是在2016年有幸倒騰Java的時候第一次接觸,當時對這兩個概念很是模糊;后來由於各種原因又回到.net 大本營,又再次接觸了IOC和DI,也算終於搞清楚了IOC和DI 這兩個概念關系。使用過ASP.NET Core的人對這兩個概念一定不陌生,想必很多人還是很難去理解這兩個東西,所以,趁着今天有空,就去把兩個概念捋清楚,並將學習過程的知識點記錄下來。
一、概念
1.1 什么是IOC?
Ioc—Inversion of Control,即控制反轉
,其是一種設計思想
,而不是一種技術。再沒有使用IOC之前,我們一般是通過new來實例化,從而創建一個對象。但是我們使用IOC之后,創建這個對象的控制權將由內部轉換到外部,那么這個過程便可以理解為控制反轉。也即把對象轉換成抽象對象的依賴
.。
同時控制反轉也是一個目標,控制反轉的優點有如下兩點:
- 可以很好的做到
解耦
屏蔽對象的實現細節
,只關心動作不關心動作中的細節。
1.2 什么是DI(依賴注入)?
全稱為Dependency Injection
,意思自身對象中的內置對象是通過注入的方式進行創建。形象的說,即由容器動態的將某個依賴關系注入到組件之中。
1.3 IOC和DI的聯系?
IOC是一種設計思想,而DI是這種設計思想的一個實現。理解IOC和DI的關鍵是:“誰依賴誰,為什么需要依賴,誰注入誰,注入了什么”。
●誰依賴於誰:當然是應用程序依賴於IoC容器;
●為什么需要依賴:應用程序需要IoC容器來提供對象需要的外部資源;
●誰注入誰:很明顯是IoC容器注入應用程序某個對象,應用程序依賴的對象;
●注入了什么:就是注入某個對象所需要的外部資源(包括對象、資源、常量數據)
1.4 常見的IOC框架。
微軟.net core 內置的DI、Autofac、Unity
以上已經把IOC和DI 這兩個聯系簡要捋清楚了,下面我們一起學習.net core 內置的DI使用。
二、內置IOC
2.1 內置的IOC 有三種生命周期
Transient
:瞬時生命周期, Transient服務在每次被請求時都會被創建一個新的對象。這種生命周期比較適用於輕量級的無狀態服務。Scoped
: Scoped生命周期的服務是每次web請求被創建,局部單例對象, 在某個局部內是同一個對象(作用域單例,本質是容器單例);一次請求內是一個單例對象,多次請求則多個不同的單例對象.Singleton
: Singleton生命能夠周期服務在第一被請求時創建,在后續的每個請求都會使用同一個實例。如果你的應用需要單例服務,推薦的做法是交給服務容器來負責單例的創建和生命周期管理,而不是自己來走這些事情。
我們先來看一張圖:
ASP.NET Core本身已經集成了一個輕量級的IOC容器
,開發者只需要定義好接口后(抽象),並且對抽象的接口進行實現,再Startup.cs的ConfigureServices方法里使用對應生命周期的注入,再調用的地方進行使用,比如構造函數注入等等。
在startup
類中ConfigureServices
方法對實例進行注冊如下代碼:
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
Console.WriteLine("ConfigureServices");
services.AddControllersWithViews();
//注入生命周期為單例的服務
services.AddSingleton<ISingletonService, SingletonService>();
//注入生命周期為Scoped 的服務
services.AddScoped<IScopedService, ScopedService>();
//注入生命周期為瞬時的服務
services.AddTransient<ITransientService, TransientService>();
}
上面代碼我分別注冊了單例、瞬時、作用域的生命周期的服務。
下面簡單寫了一個例子讓大家看看這三個生命周期的實例的代碼
三個生命周期的抽象服務實現代碼如下:
public class ScopedService : IScopedService
{
public string GetInfo()
{
return $"this is scoped service ";
}
}
public class SingletonService : ISingletonService
{
public string GetInfo()
{
return $"this is singleton service";
}
}
public class TransientService : ITransientService
{
public string GetInfo()
{
return $"this is transient service";
}
}
控制器代碼如下:
public IActionResult Index()
{
using (IServiceScope scope = HttpContext.RequestServices.CreateScope())
{
var transientService = scope.ServiceProvider.GetService<ITransientService>();
var transientService2 = scope.ServiceProvider.GetService<ITransientService>();
var result = $"{transientService.GetInfo()} hashCode : {transientService.GetHashCode()} <br/>";
result += $"{transientService2.GetInfo()} hashCode : {transientService2.GetHashCode()} <br/>";
ViewBag.Transient = result;
var scopeService= scope.ServiceProvider.GetService<IScopedService>();
var scopeService2 = scope.ServiceProvider.GetService<IScopedService>();
result = $"{scopeService.GetInfo()} hashCode :{ scopeService.GetHashCode()} <br/>";
result += $"{scopeService2.GetInfo()} hashCode :{ scopeService2.GetHashCode()} <br/>";
ViewBag.Scope = result;
var singletonService = scope.ServiceProvider.GetService<ISingletonService>();
var singletonService2 = scope.ServiceProvider.GetService<ISingletonService>();
result = $"{singletonService.GetInfo()} hashCode:{ singletonService.GetHashCode()} <br/>";
result += $"{singletonService2.GetInfo()} hashCode:{ singletonService2.GetHashCode()} <br/>";
ViewBag.Singletion = result;
}
return View();
}
index.cshtml 視圖代碼如下:
@{
ViewData["Title"] = "Home Page";
}
<b>Transient生命周期</b>
<div>
@Html.Raw(ViewBag.Transient)
</div>
<b>Scoped生命周期</b>
<div>
@Html.Raw(ViewBag.Scope)
</div>
<b>Singletion生命周期</b>
<div>
@Html.Raw(ViewBag.Singletion)
</div>
分別運行兩次的結果如下圖:
從上圖的運行的每個對象的hashCode 的結果看出Transient
生命周期是每次獲得對象都是一次新的對象;Scoped
生命周期是在作用域是同一個對象,非作用域內則是新的對象;Singletion
生命周期是最好理解的,是這個服務啟動后都是一個對象,也即是全局單例對象
。
2.2 注入的幾種方式
直接注入IServiceProvider的方式
services.AddSingleton
();
然后在構造函數中通過如下方式獲取具體實現
public HomeController(IServiceProvider serviceProvider)
{
var singletonService = serviceProvider.GetService<SingletonService>();
}
通過GetServices方式
services.AddSingleton<ISingletonService, SingletonService>();
然后在構造函數中通過如下方式獲取具體實現
public HomeController(IServiceProvider serviceProvider)
{
var singletonService = serviceProvider.GetService<ISingletonService>();
}
構造函數直接注入方式(推薦)
public HomeController(ISingletonService singletonService)
{
var _singletonService =singletonService;
}
集合方式注入
這種方式其實就是省去了注入IServiceProvider
的過程,直接將GetServices
獲取的結果進行注入。首先注入interface
及具體實現
services.AddSingleton<ISingletonService, SingletonService1>();
services.AddSingleton<ISingletonService, SingletonService2>();
獲取的方式如下
public HomeController(IEnumerable<ISingletonService> services)
{
var singletoService1 = services.First();
var singletoService2 = services.Skip(1).First();
}
工廠方式注入
然后我們繼續注入Func這個工廠,這里我們按int
來返回不同的實現,當然你也可以采用其他方式比如string
services.AddSingleton(provider =>
{
Func<int, ISingletonService> func = n =>
{
switch (n)
{
case 1:
return provider.GetService<SingletonService1>();
case 2:
return provider.GetService<SingletonService2>();
default:
throw new NotSupportedException();
}
};
return func;
});
然后在構造函數中通過如下方式獲取具體實現
public HomeController(Func<int, ISingletonService> funcFactory)
{
var singletonService1 = funcFactory(1);
var singletonService2 = funcFactory(2);
}
除了以上的幾個注入方式外,還可以通過反射的方式批量注入程序集的方式,這里就不一一寫出具體的例子,自己去嘗試。
三、IOC怎么解耦?
學習到這里,大家對IOC和DI 的使用已經有了一定的掌握,上面我提到過IOC
的目標是解耦
、屏蔽對象的實現細節
這兩大優點;再來回顧上面的代碼實現 可以發現,推薦的注入方式是通過抽象接口
的方式進行注入而不是直接注入對象方式。
現在我列舉一個企業發展過程中很常見的一個例子,比如:我在一家企業擔任開發工作,開發了一個電商平台系統,系統中需要用到日志系統,由於當時的各種外在環境,我們使用的日志是nlog
這個日志組件;但是經過平台的不斷發展后,nlog 日志組件已經不能滿足我們平台的需求,需要尋求更智能的日志系統,比如Exceptionless
,這時候我們就不得不權衡下現有代碼的可維護性。剛好這個電商平台系統代碼使用了IOC 使得代碼可維護性比較強,日志系統耦合性比較低,只需要簡單的幾行代碼即可實現日志系統的大換血。現在來看下電商系統目前使用的日志系統相關的代碼。
日志組件服務注冊如下代碼:
services.AddSingleton<ILogService, nLogService>();
各業務中使用nlog代碼大概如下:
public HomeController(ILogService LogService)
{
_logService =LogService;
_logService.Info("=========開始訪問========");
}
從上面的代碼中使用日志的相關業務代碼都是通過IOC來進行控制反轉調用日志服務,隱藏了日志服務業務的實現細節;使用業務方無需關注日志的實現細節,從而達到 了高度解耦的效果-屏蔽對象實現細節
。
現在我們進行日志系統大換血代碼只需要實現一個新的日志服務,我這里創建ExceptionlessLogService
類繼承ILogService
即可,同時安排對應的人去實現ExceptionlessLogService
這個類就可以達到日志系統升級的效果。
更換后的代碼如下:
services.AddSingleton<ILogService, NLogService>();
改成
services.AddSingleton<ILogService, ExceptionlessLogService>();
這樣就達到了一行代碼升級了整個系統的日志系統,業務調用方無需任何的改動。