[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。


<<:  .Net Core Web Api_笔记08_HTTP资源操作模式PATCH

>>:  D7(9/7)-91App(6741) 帮商家做电商的电商专家

Outlook 的 PST 档不见导致无法开启要如何修复

概述: 本教学提供了两种非常简单的Outlook修复方法,解决了Outlook PST文件找不到导致...

【C#】Behavioral Patterns Visitor Mode

The Visitor design pattern represents an operation...

[Day12] Esp32s用AP mode + LED - (认识序列埠监控视窗&程序码讲解)

1.前言 看完上一篇的利用Esp32s的AP mode控制LED灯了吧,是不是觉得很神奇阿,那本篇会...

取得Microsoft Graph API 验证 token - MSAL

说明 继上篇建立 App Registration 後,本篇将继续介绍使用MSAL透过Activat...

(Day 19) 原型与建构式

函式建构式建立原型 前面几篇有提到,可以使用函示建构式、或是 ES 6 来建立原型,今天就来介绍使用...