iT邦幫忙

2021 iThome 鐵人賽

DAY 8
1
Modern Web

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

[Day08] Dependency Injection Part2 - 依賴介面

依賴介面而不是特定的 Service

昨天我們介紹了怎麼在 .NET Web API 的專案裡實現依賴注入,但是昨天我們注入的是一個指定的 Service。這樣的情況下我們其實無法感受到 DI 的好處,用起來的感覺只是把 new 出 Service 實體的程式碼集中到建構式,如果哪天這個 Service 有變動,依賴這個 Service 的地方還是都要改。比較好的作法是讓我們的 Controller 依賴介面(interface)而不是依賴固定的 Service 類別,今天我們要來修改我們的程式,發揮 DI 的好處!

什麼是介面(interface)

在開始之前,我們先介紹一下介面,以下節錄微軟官網對介面的解釋

An interface contains definitions for a group of related functionalities that a non-abstract class or a struct must implement.

介面包含了一組相關的功能定義,非抽象的類別或結構必須實作這些定義。

(官網的繁體中文翻譯真的很爛,所以我自己翻了一下 = =)

不過我想除非真的實際定過、實作過介面,不然還是無法明白這句話在說什麼。這裡,筆者提供一個自己的理解給大家參考:「介面定義了一組相關的方法(method),一個類別如果實作這個介面,那麼這個類別就必須依照介面的定義完成所有的方法」

舉個例子來說,我們定義了一個叫「貓咪」的介面,包含吃罐頭()、睡覺()、呼嚕嚕()三個方法,然後我們讓一個「橘貓」類別實作這個介面,那麼「橘貓」就一定至少要有吃罐頭()、睡覺()、呼嚕嚕()三個公開的(public)方法,而且參數、回傳的資料型態一定要與介面的定義相同

public interface ICat // 介面的命名慣例會在最開頭加大寫 I
{
    void Eat(); // 吃罐頭
    void Hoolulu(); // 呼嚕嚕
    int Sleep(int hour); // 睡覺
}
public class OrangeCat : ICat
{
    public void Eat()
    {
        Console.WriteLine("橘貓開始吃罐頭");
    }

    public void Hoolulu()
    {
        Console.WriteLine("橘貓開始呼嚕嚕");
    }

    public int Sleep(int hour)
    {
        Console.WriteLine("橘貓睡了" + hour + "小時");
        return hour;
    }
}

public class ScottishFold : ICat
{
    public void Eat()
    {
        Console.WriteLine("摺耳貓開始吃罐頭");
    }

    public void Hoolulu()
    {
        Console.WriteLine("摺耳貓開始呼嚕嚕");
    }

    public int Sleep(int hour)
    {
        Console.WriteLine("摺耳貓睡了" + hour + "小時");
        return hour;
    }
}

實作介面的好處

讓我們的類別實作介面有幾個直接的好處

  1. 使用這個類別的人很直接能知道這個類別有什麼功能,例如我們知道「橘貓」類別實作了「貓咪」介面,我們就能知道它一定有吃罐頭()、睡覺()、呼嚕嚕()三個公開的方法。
  2. 使用這個類別的程式,可以無痛的被其他實作了「貓咪」介面的類別取代,例如換上一隻「摺耳貓」,我們的程式一樣能讓它吃罐頭()、睡覺()、呼嚕嚕()。待會我們就要利用這個好處來改良我們的 DI
  3. 可以把所有實作了同一個介面的類別放到同一個資料集合(例如 Array 或 List),用一個迴圈就能處理(?)所有貓咪
var rand = new Random();
var cats = new List<ICat>();
for (int i = 0; i < 10; i++)
{
    if (rand.Next() % 2 == 0)
    {
        cats.Add(new OrangeCat());
    }
    else
    {
        cats.Add(new ScottishFold());
    }
}

foreach (ICat cat in cats)
{
    cat.Hoolulu();
    cat.Eat();
    cat.Sleep(1);
}

修改我們的 Service

  1. 定義一個介面
    首先,我們定義一個 IUserCRUD 介面。
public interface IUserCRUD
{
    public List<User> GetAllUsers();
    public User GetUserById(int id);
    public void CreateUser(User model);
    public void UpdateUser(int id, User model);
    public void DeleteUser(int id);
}

然後,讓我們的 Service 實作這個介面

public class UserService : IUserCRUD
{
    // 內容不變
}
  1. 在 Startup.cs 註冊介面
    把註冊 Service 的程式改成下面的程式碼。角括號裡的第一個參數代表我們要註冊 IUserCRUD 這個介面讓別人依賴,而實作這個介面的類別是 UserService。
services.AddScoped<IUserCRUD, UserService>();
  1. 在 Controller 注入介面
    把 Controller 裡注入明確指定的類別(UserService)的程式,改成注入介面
public class UserController : ControllerBase
{
    private readonly IUserCRUD _user;
    public UserController(IUserCRUD user)
    {
        _user = user;
    }
    
    // 其他的程式不變
}

這樣一來,我們就成功的讓 Controller 依賴介面而不是明確指定的類別了!執行程式可以發現,API 用起來完全一樣。

試試看替換 Service

現在,假設我們新增一個實作 IUserCRUD 介面的 Service,這個 Service 從本地端的檔案讀寫 User 資料

public class UserServiceWithFile : IUserCRUD
{
    private readonly string _fileName = "D:/TestUsers.csv";
    private readonly List<User> _users;

    private List<User> ReadUsersFromFile(string fileName)
    {
        if (!File.Exists(_fileName))
        {
            var file = File.Create(_fileName);
            file.Close();
        }

        var users = new List<User>();
        using (var reader = new StreamReader(fileName))
        {
            while (!reader.EndOfStream)
            {
                var values = reader.ReadLine().Split(",");
                users.Add(new User()
                {
                    UserId = Convert.ToInt32(values[0]),
                    UserName = values[1],
                    Email = values[2]
                });
            }
        }
        return users;
    }

    private void SaveUsersToFile(string fileName)
    {
        using (var writer = new StreamWriter(fileName))
        {
            foreach (var user in _users)
            {
                writer.WriteLine($"{user.UserId},{user.UserName},{user.Email}");
            }
        }
    }

    public UserServiceWithFile()
    {
        _users = ReadUsersFromFile(_fileName);
    }

    public List<User> GetAllUsers()
    {
        return _users;
    }

    public User GetUserById(int id)
    {
        return _users.FirstOrDefault(x => x.UserId == id);
    }

    public void CreateUser(User model)
    {
        if (_users.Count == 0)
        {
            model.UserId = 1;
        }
        else
        {
            model.UserId = _users.Max(x => x.UserId) + 1;
        }
        _users.Add(model);
        SaveUsersToFile(_fileName);
    }

    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;
        }
        SaveUsersToFile(_fileName);
    }

    public void DeleteUser(int id)
    {
        var existingUser = _users.FirstOrDefault(x => x.UserId == id);
        if (existingUser != null)
        {
            _users.Remove(existingUser);
        }
        SaveUsersToFile(_fileName);
    }
}

接著,把註冊 Service 的地方改成

services.AddScoped<IUserCRUD, UserServiceWithFile>();

執行程式一樣會覺得跑起來一模一樣!而且只要在註冊 Service 的地方稍作修改,我們就能隨時替換真正使用的 Service,依賴這個介面的其他程式完全不用改!我們在後面的文章會使用 MySQL 來管理資料,到時候只要再新增一個 UserServiceWithMySQL 實作 IUserCRUD 介面,然後註冊的地方換一下,就能無痛把用檔案管理使用者的 Service 替換掉!沒騙你吧!學 DI 的 CP 值真的很高!

明天我們將繼續使用 DI 的方法,把程式需要的組態設定注入給 Controller。


上一篇
[Day07] Service 與 Dependency Injection (依賴注入)
下一篇
[Day09] 從 appsettings.json 取得設定
系列文
網站一條龍 - 從架站到前端33

1 則留言

0
tomasysh
iT邦新手 5 級 ‧ 2021-09-13 12:22:42

弱弱地舉手問:「依賴介面而不是特定的 Service」 ...這就是傳說中的「依賴反轉原則」嗎?~

goattl iT邦新手 5 級 ‧ 2021-09-13 17:08:33 檢舉

弱弱的回答:是滴~~ DI 的目的就是為了達到 IoC
(竟然要先解新手任務才能回覆...)

我要留言

立即登入留言