iT邦幫忙

2021 iThome 鐵人賽

DAY 7
1
Modern Web

網站一條龍 - 從架站到前端系列 第 7

[Day07] Service 與 Dependency Injection (依賴注入)

什麼是依賴注入

在說明什麼是 Dependency Injection(DI, 依賴注入)前,要先來介紹一下什麼是「依賴」。依賴簡單的說就是當前這段程式(class 或 function)所需要的外部功能。而「依賴注入」就是,不要讓這段程式主動建立「依賴」的實體,被動的從外部接收這個「依賴」。以 .NET 為例,就是不要自己 new class 實體來用,而是讓 Startup.cs 裡的服務容器把「依賴」拿給你。

記得以前學生時代寫扣的時候,根本不懂軟體工程(謎之聲:你以為你現在懂嗎?),寫了幾個 class,哪裡要用就在哪裡呼叫建構式產生實體。當時因為是一人專案而且專案規模小,所以也沒碰上什麼大麻煩。甚至曾經看著 Stack Overflow 上的這個回答呵呵笑。

後來畢業工作之後,痛苦就開始了,當時的同事跟當時的我走同樣的風格,大家一起煮義大利麵,曾經有一個 function 將近一千行,裡面有我的 class 也有他的 class。當我們各自的 class 需又有所變更的時候,就會發現,完了!改不動!一改要嘛會出錯,要嘛一大堆地方都要改!

後來實際嘗試使用 DI,才真正體會到他的美好,筆者個人覺得 DI 真的是軟體工程裡 CP 值很高的一種設計,學起來不會很難,但是對將來的維護幫助很大。

如果想要了解更多關於依賴注入的知識,可以參考這篇文章

在專案裡使用 DI

通常我們要注入的東西,會以一個「Service」為單位,一個 Service 提供某個工作分類的服務,我們可以在開發中的程式裡叫用這些服務來達成目的。例如一個下訂單的 API,可能會需要幾個 Service 來完成任務

  1. UserService - 確認使用者是否已經登入
  2. ProductService - 確認商品庫存
  3. OrderService - 新增訂單
  4. LogService - 寫入 Log

所以在我們實作 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 的時候就會決定他們的生命週期:

  • AddSingleton - 所有的 Request 共用一個 Service 實體
  • AddScoped - 同一個 Request 裡,只會共用一個 Service 實體。
  • AddTransient - 不管是不是同一個 Request,只要需要注入的地方就會產生新的實體

與資料庫操作相關的 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


上一篇
[Day06] 用 .NET 實做簡單的 RESTful API
下一篇
[Day08] Dependency Injection Part2 - 依賴介面
系列文
網站一條龍 - 從架站到前端33

尚未有邦友留言

立即登入留言