【译】如何在TypeScript中使用依赖注入

639 阅读7分钟

「这是我参与2022首次更文挑战的第15天,活动详情查看:2022首次更文挑战」。

前言

本文主要讲述如何使用TypeScript在Node.js环境中应用依赖注入。

介绍

每个软件程序都由基础模块构成而成。在面向对象编程范式中,我们使用类来构建复杂的架构。类似于使用积木搭建房子,我们将互相之间有联系的模块之间互相称之为“依赖”。这些被依赖的类为我们的提供了一系列复杂的操作或者行为。

一个类中可能含有其他的类作为其成员。那么,我们会面临以下几个问题:

  1. 这些依赖应该怎样被构建?是由我们组装这些对象,还是应该由其他程序来实例化他们?
  2. 倘若实例化这个类过于复杂,并且我们希望避免一些“面条式”的代码应该怎么办?

而**依赖注入(Dependency Injection)**就是为了上述问题。

在开始我们的例子之前,我们需要弄懂几个关于依赖注入的概念。依赖注入的思想告诉我们一个类应该将它的依赖作为参数,而不是直接实例化它们。你可以将复杂的部分从代码中抽取出来,并以另一种方式重新引入依赖项。但是如何抽取复杂逻辑和重新引入依赖项是依赖管理所面临的问题。你可以手动的操作所有的实例化和注入操作,但是这必然会导致一个十分复杂的系统,这是我们需要避免的情况。取而代之的是,你应该将创建对象的责任交给IoC容器(Inverse of Control 控制反转)

控制反转就是让一个容器能够控制程序依赖的所有依赖项。我们创建一个容器,该容器负责所有对象的创建工作。当一个类需要被实例化时,容器就会增加所需的依赖项。

IoC仅仅只是表达了一种方法,而不是一种具体的实现。为了应用依赖注入这种思想,我们需要一种DI框架。典型的DI框架有如下:

  • Spring & Dagger —— Java
  • Hilt —— Kotlin
  • Unity —— C#
  • Inversify, Nest.js, TypeDI —— TypeScript

总览 & 角色

image.png

在依赖注入的规则中,我们需要明白以下四种不同的角色的类型

  • Client
  • Service
  • Interface
  • Injector

Service是我们需要暴露的对象。该类被IOC容器管理并实例化。Client可以通过IOC容器来使用services。client不关心services中的具体细节,interface 能够确保client和service能够保持同步。当client需要依赖时,injector能够提供实例化的service。

依赖注入的方式:

我们能够以以下三种方式进行依赖注入:

  • 我们能够通过 properties(field) 来应用依赖注入。在类中定义一个属性并在这个属性被调用时注入所需要的依赖。但是将属性暴露出来,违反了OOP的原则,因此,应该尽可能的避免这种类型的依赖注入。

  • 我们还能够通过 methods 来应用依赖注入。一个对象的状态应该是私有的,当外部想要改变这个状态时,那么应该使用其 getter/setter 方法。当你使用 setter 方法来实例化一个类的私有成员时,这就是方法注入

  • 还可以通过 constructor 来应用依赖注入。该方法与构造一个对象的方式高度相似,且依赖关系清晰。所以我们通常推荐使用该种注入依赖的方式

使用TypeDI

只要我们明白了依赖注入的底层原理,那么不管使用哪个框架对于我们来说都没有太大的差异。本篇文章中,我选择了使用TypeScript编程语言和 TypeDI框架来实现一个简单的Demo来说明这些基础概念。

初始化项目可能需要花费一点时间,所以我这里提供一些简单的初始代码模板。你可以子这个GitHub仓库下载代码:mertturkmenoglu/typescript-dependency-injection: Medium TypeScript Dependency Injection Article Codes (github.com)

任意的TypeScript项目都可以用于演示DI,本文中使用了 Express App来进行演示。本文假设读者已经具有了基本的Express和 TypeScript的知识了。

该项目的入口文件为 src/index.ts ,该文件包含了启动项目的具体步骤:

import 'reflect-metadata';

import express from 'express';
import Container from 'typedi';
import UserController from './controllers/UserController';

const main = async () => {
  const app = express();

  const userController = Container.get(UserController);

  app.get('/users', (req, res) => userController.getAllUsers(req, res));

  app.listen(3000, () => {
    console.log('Server started');
  });
}

main().catch(err => {
  console.error(err);
});

上述代码是一个非常简单的Express服务器,它仅仅拥有一个接口。当发起GET请求到 /user 路由时,会返回一个用户的列表。关键性的函数在 Container.get 方法。注意这里,我们并没有使用 new 关键字来实例化一个对象。我们仅仅只是要求IOC容器给我们一个 UserController 的实例。然后我们将相关的路由和控制器方法绑定在一起。

该应用虽然是一个用于演示的项目,但是我不希望它完全没有实际意义。我增加了不同的文件夹来表示后端的基础架构。它们分别是 控制器层(controller), 模型层(model), 数据层(repositories)服务层(services)。现在,让我们看看它们的作用。

  • Controllers 目录包含了 REST风格的控制器,它们主要负责在用户和服务端之间进行沟通,它们接受用户的请求并返回结果。
  • Models 目录包含了我们的数据库实体。虽然我们没有数据库链接,我们目前也不需要数据库,但是建立合适的项目结构有利于我们的学习。我们假设我们已经拥有了一个数据库实体。
  • Services 目录包含了我们的服务。他们主要为Controller所服务来读取不同的repositories。
  • Repositories 目录包含了读取数据库的类。我们使用数据映射技术来对数据库进行相关的操作。在该模式下,我们使用相关的类来读取数据库并进行一些操作。

我们不需要在一个类中做所有的事情。在请求和响应之间被划分成了许多层。这被称为 layered architechture。在不同的类中进行分工让我们更容易进行依赖注入。

import { Request, Response } from "express";
import { Service } from "typedi";
import UserService from "../services/UserService";

@Service()
class UserController {
  constructor(private readonly userService: UserService) { }
  async getAllUsers(_req: Request, res: Response) {
    const result = await this.userService.getAllUsers();
    return res.json(result);
  }
}

export default UserController;

UserController 仅仅只有一个方法。 "getAllUsers" 方法从 user service 中获取结果并且将结果返回。我们为 UserController 类增加了一个 @Service 装饰器,这表示将这个类交由IOC容器进行管理。在构造函数中,我们可以看到该类需要一个 UserService 实例。再次强调,我们不需要控制依赖的实例化,因为 TypeDI 容器在生成UserController实例时会帮助我们创建 UserService 实例,也就是说会注入 UserService

import { Service } from "typedi";
import User from "../models/User";
import UserRepository from "../repositories/UserRepository";

@Service()
class UserService {
  constructor(private readonly userRepository: UserRepository) { }
  async getAllUsers(): Promise<User[]> {
    const result = await this.userRepository.getAllUsers();
    return result;
  }
}

export default UserService;

UserServiceUserController 十分类似,我们为其增加一个 @Service 装饰器,并且我们在构造函数的参数列表中指定了需要依赖的类。

import { Service } from "typedi";
import User from "../models/User";

@Service()
class UserRepository {
  private readonly users: User[] = [
    { name: 'Emily' },
    { name: 'John' },
    { name: 'Jane' },
  ];

  async getAllUsers(): Promise<User[]> {
    return this.users;
  }
}

export default UserRepository;

UserRepository是我们的最后一个类。同样为类增加装饰器。但是这个类没有任何需要依赖的类。因为我们现在没有数据库链接,此处我仅仅只是硬编码了一个简单的用户列表。

总结

依赖注入为复杂的对象实例化提供了便利。虽然手动的进行依赖注入聊胜于无,但是使用 TypeDI 提供的IOC容器使这一切变得更加的容易和简单。

感谢您的时间阅读本文~

原文地址

Dependency Injection in TypeScript | by Mert Türkmenoğlu | Level Up Coding (gitconnected.com)