[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="[email protected]"},
        new User() {UserId = 1, UserName = "Bob", Email="[email protected]"},
        new User() {UserId = 2, UserName = "Cathy", Email="[email protected]"},
    };

    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


<<:  Day 05 | 资料绑定(一)

>>:  Tcl语言和你 SAY HELLO!!

Day 3 - 如何运用Laravel框架设计模式规划大型专案

Laravel是基於MVC架构设计出来的框架, 什麽是MVC(Model–View–Controll...

{DAY 3}如何处理一笔数据?(下)

前言 第三天要延续介绍如何处理一笔数据。 对於数据分析的用途跟前几个步骤 请看上一篇文章 数据分析...

[day-2] 基础Python介绍,何谓Python以及它的实际用途

为何选择Python ?而不是其他的语言。 每个程序语言都有属於它们的专长,Python是一种高阶语...

Day 19 | 万年范例-TodoList(2)

回到昨天留下的问题 card数太长要怎麽办 TextFiled 送出後怎麽清除里面的字 其实只要将 ...

3分钟帮你找到部落格要写的内容

一、前言 网站该写些什麽内容?这点对於部落格新手来说,刚开始因为有许多想分享的东西,有什麽写什麽,...