前兩節我們已經學會了依賴注入的基本用法,這節要來教IoC控制反轉
這是稍微進階的技巧,但卻又是很基本的觀念,所以還是要教給大家
剛學依賴注入的時候,可能會覺得不知道好處在哪裡,如果只是要拆分程式,似乎只要另寫類別就好,不用特別使用依賴注入
但其實使用依賴注入主要有兩個重點,第一個就是生命週期的管理,你不用在擔心生命週期的問題,總之生命週期就會依注入的方式決定
第二就是可以使用一些進階的設計模式,例如這節要教的控制反轉Ioc
這邊有兩種情況來示範,第一種是我們今天想要將原有的程式去做大修改
那如果沒有依賴注入,我們可能會將原有的程式清掉,寫上新的,又或者註解起來,但這樣一個是會找不到舊的寫法,一個會使程式雜亂
所以這時就可以使用依賴注入的控制反轉的技巧來寫
那假設我們今天要將原有的linq處理資料的方式改成用sql的方式
那首先我們要先建立一個介面INewsService.cs

namespace Kcg.Interface
{
public interface INewsService
{
Task<List<NewsDto>> 取得新聞列表();
Task<NewsDto> 取得新聞詳細資訊(Guid newsId);
Task 新增(NewsCreateDto news);
Task<NewsEditViewModel> 修改取得(Guid? id);
Task 修改(Guid id, NewsEditDto news);
Task 刪除(Guid id);
}
}
至於介面的基本觀念,網路上已經很多了,我這邊就不在說明了
接著將我們原來的NewsService.cs繼承該介面
public class NewsService : INewsService
{}
這邊說明一下,正常的流程應該是先定義介面,再去寫實作,但因為我們這邊實作已經寫好了所以才會這樣
接著我們要來寫使用sql處理資料的實作,先建立一個NewsSqlService.cs並繼承INewsService
namespace Kcg.Services
{
public class NewsSqlService : INewsService
{
private readonly KcgContext _context;
public NewsSqlService(KcgContext context)
{
_context = context;
}
public async Task<List<NewsDto>> 取得新聞列表()
{
string sql = @"SELECT a.Click,
a.Enable,
a.EndDateTime,
a.NewsId,
a.StartDateTime,
Title = a.Title + 'sql',
a.UpdateDateTime
FROM News a";
var result = await _context.News.FromSqlRaw(sql)
.Select(x => new NewsDto
{
Click = x.Click,
Enable = x.Enable,
EndDateTime =x.EndDateTime,
NewsId = x.NewsId,
StartDateTime = x.StartDateTime,
Title = x.Title,
UpdateDateTime = x.UpdateDateTime
}).ToListAsync();
return result;
}
public Task 修改(Guid id, NewsEditDto news)
{
throw new NotImplementedException();
}
public Task<NewsEditViewModel> 修改取得(Guid? id)
{
throw new NotImplementedException();
}
public Task 刪除(Guid id)
{
throw new NotImplementedException();
}
public Task<NewsDto> 取得新聞詳細資訊(Guid newsId)
{
throw new NotImplementedException();
}
public Task 新增(NewsCreateDto news)
{
throw new NotImplementedException();
}
}
}
這邊我們就示範一個就好,所以只改寫第一個取新聞列表資料的部分
接著我們NewsController.cs要改一下取得的服務,改成取得INewsService
private readonly INewsService _newsService;
public NewsController(INewsService newsService)
{
_newsService = newsService;
}
最後我們到program.cs改寫成控制反轉的注入方式
// 原來是這樣
// builder.Services.AddScoped<NewsService>();
// 如果有介面的話改這樣
// builder.Services.AddScoped<INewsService, NewsService>();
// 改成sql處理資料
builder.Services.AddScoped<INewsService, NewsSqlService>();
如果以後我們要改實作方式,只要改第二個欄位的類別,就可以將實作方式進行替換,我們就不需要把之前的程式清掉,或用註解的方式去改程式了
來看一下結果

我這邊在標題後面加上sql,代表我真的是改成用sql取得資料了
第二種就是如果我今天在本機沒辦法連上資料庫而改用mock資料測試的話,上線時則要真的連到資料庫
那這時候該怎麼寫呢?其實也很簡單,我們可以這樣寫
if (builder.Environment.IsDevelopment())
{
builder.Services.AddScoped<INewsService, NewsMockService>();
}
else
{
builder.Services.AddScoped<INewsService, NewsService>();
}
如此就可以簡單又很清晰地去做切換,如果用其他方式處理,都會增加後續維護上的複雜度
而且使用介面還有一個好處,就是以後要改實作,只需要改program這裡的設定,原來程式的地方是完全不需要修改的,因為方法名稱都相同
那這邊如果有在網路上看過依賴注入的人,可能會發現很多都是教界面加類別的注入方式,但卻沒有說為什麼
// 我一開始教的範例
// builder.Services.AddScoped<NewsService>();
// 網路上大部分的範例
// builder.Services.AddScoped<INewsService, NewsService>();
其實,沒要使用這些進階技巧實,是完全不需要使用介面的依賴注入方式
而網路上的範例舉的例子也往往看不到用介面的用意,新手更有可能以為依賴注入就是要這樣寫,因此寫了一堆無用的介面
那這個時候可能會問,那究竟什麼時候要寫介面,什麼時候不用寫介面,我只能說這就很吃自己的實務經驗判斷,沒有一個正確的答案,只有適合的情境
不過我這邊建議,一開始如果你還不清楚該不該用的時候,一律不使用介面的方式進行依賴注入,等哪天真的需要用了,我們再改過來
這樣做的原因,一來是可以減少很多不必要的介面,二來你有改過才會有深刻記憶說這個地方原來會很需要用到介面
範例檔:下載