在說明什麼是 Dependency Injection(DI, 依賴注入)前,要先來介紹一下什麼是「依賴」。依賴簡單的說就是當前這段程式(class 或 function)所需要的外部功能。而「依賴注入」就是,不要讓這段程式主動建立「依賴」的實體,被動的從外部接收這個「依賴」。以 .NET 為例,就是不要自己 new class 實體來用,而是讓 Startup.cs 裡的服務容器把「依賴」拿給你。
記得以前學生時代寫扣的時候,根本不懂軟體工程(謎之聲:你以為你現在懂嗎?),寫了幾個 class,哪裡要用就在哪裡呼叫建構式產生實體。當時因為是一人專案而且專案規模小,所以也沒碰上什麼大麻煩。甚至曾經看著 Stack Overflow 上的這個回答呵呵笑。
後來畢業工作之後,痛苦就開始了,當時的同事跟當時的我走同樣的風格,大家一起煮義大利麵,曾經有一個 function 將近一千行,裡面有我的 class 也有他的 class。當我們各自的 class 需又有所變更的時候,就會發現,完了!改不動!一改要嘛會出錯,要嘛一大堆地方都要改!
後來實際嘗試使用 DI,才真正體會到他的美好,筆者個人覺得 DI 真的是軟體工程裡 CP 值很高的一種設計,學起來不會很難,但是對將來的維護幫助很大。
如果想要了解更多關於依賴注入的知識,可以參考這篇文章
通常我們要注入的東西,會以一個「Service」為單位,一個 Service 提供某個工作分類的服務,我們可以在開發中的程式裡叫用這些服務來達成目的。例如一個下訂單的 API,可能會需要幾個 Service 來完成任務
所以在我們實作 DI 之前,我們先來把 Controller 裡的處理邏輯抽出來放到一個 Service。筆者習慣在專案底下新增一個 Services 資料夾,再把我們的 Service 加進去
public class UserService
{
private static List<User> _users = new List<User>()
{
new User() {UserId = 0, UserName = "Alice", Email="alice@test.mail"},
new User() {UserId = 1, UserName = "Bob", Email="bob@test.mail"},
new User() {UserId = 2, UserName = "Cathy", Email="cathy@test.mail"},
};
public List<User> GetAllUsers()
{
return _users;
}
public User GetUserById(int id)
{
return _users.FirstOrDefault(x => x.UserId == id);
}
public void CreateUser(User model)
{
model.UserId = _users.Max(x => x.UserId) + 1;
_users.Add(model);
}
public void UpdateUser(int id, User model)
{
var existingUser = _users.FirstOrDefault(x => x.UserId == id);
if (existingUser != null)
{
existingUser.UserName = model.UserName;
existingUser.Email = model.Email;
}
}
public void DeleteUser(int id)
{
var existingUser = _users.FirstOrDefault(x => x.UserId == id);
if (existingUser != null)
{
_users.Remove(existingUser);
}
}
}
接著,在 Startup.cs 把這個 Service 註冊到服務容器。.NET 的 DI 框架會幫我們管理 Service 的生命週期,在註冊 Service 的時候就會決定他們的生命週期:
與資料庫操作相關的 Service 通常會用 AddScoped,所以我們在 ConfigureServices 這個 method 裡註冊剛剛寫的 Service
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Ithome_2021_API", Version = "v1" });
});
// 加入這一行
services.AddScoped<UserService>();
}
再來,在 Controller 加入建構式,只要 .NET 發現建構式需要東西當參數,.NET就會嘗試從服務容器裡找出這個東西,並把他「注入」給這個 Controller。注入之後用一個 private 變數存起來,後面的程式就能使用這個 Service,最後把抽掉程式碼造成的 error 修一修,簡單的 DI 實作就完成了
[Route("api/[controller]")]
[ApiController]
public class UserController : ControllerBase
{
private readonly UserService _user;
public UserController(UserService user)
{
_user = user;
}
// GET: api/<UserController>
[HttpGet]
public IEnumerable<User> Get()
{
return _user.GetAllUsers();
}
// GET api/<UserController>/5
[HttpGet("{id}")]
public User Get(int id)
{
var user = _user.GetUserById(id);
if (user == null)
{
throw new Exception("找不到 user");
}
return user;
}
// POST api/<UserController>
[HttpPost]
public void Post(User user)
{
_user.CreateUser(user);
}
// PUT api/<UserController>/5
[HttpPut("{id}")]
public void Put(int id, User newUserData)
{
_user.UpdateUser(id, newUserData);
}
// DELETE api/<UserController>/5
[HttpDelete("{id}")]
public void Delete(int id)
{
_user.DeleteUser(id);
}
}
今天的 DI 範例注入了一個明確指定的 Service class,其實這樣並沒有充分發揮 DI 的好處。明天我們會再優化我們的 DI 作法,讓 Controller 依賴「介面」而不是明確指定的 class