照片由Natalie Runnerstrom拍摄,来自Unsplash
嘿,你!对,就是你在听……你看起来像一个JavaScript开发者,对吧?充满激情和动力——你甚至阅读Medium上的Node文章。恭喜,这是迈向更好世界的良好开端。
我又有一个问题:你的Node应用中的依赖注入是怎么实现的?你有没有想过这个问题?
你是不是那种仅仅依赖 ECMAScript 模块来在整个应用程序中导入和导出函数及逻辑的开发者?随意地通过永远连接的函数的七个(或更多 😔)参数传递数据?通过六层抽象传递同样的信息?通过函数参数传递同样的信息?最终得到一个混乱的混合了函数式和其他未知架构模式的应用程序——这是 JavaScript 中非常常见的场景?
如果是的话,放心吧:你并不孤单。然而,这并不意味着不应该改变……
你没有理由再找借口了:借助 TypeScript,你可以开发出美观且面向对象的应用程序,这些应用可以依赖于适当的依赖注入。一旦你体验过这种开发方式,就再也回不到那些意大利面条般的代码了:我保证。除非它们看起来和那些一样。
照片由 Sofia Ciravegna 在 Unsplash 拍摄
会带来什么呢?
- 你将能够依赖面向对象编程的强大特性,并从中受益于封装、抽象、多态和继承。
- 你将能够应用SOLID原则,显著提升可扩展性、可测试性、可读性和可靠性。
- 你将能够控制应用程序启动时的异步任务。
让我们来探索一种实现合适的面向对象架构的方法,在Node中。不需要重新发明轮子——因此我们可以采用一种经典的方法(如果你熟悉Java,应该会感到很熟悉)。
这种方法基于以下概念如下。
控制层,服务层,仓储层模式三层对应三个问题:
- 控制器:管理 输入输出(I/O) 并确保应用程序 框架(如 REST、Kafka)与业务逻辑之间的连接。
- 服务:包含应用程序的 业务逻辑,即业务 规则 和 政策和策略。
- 仓库和客户端:整合 外部依赖项,如第三方 API。
可以在这里了解更多 👉 这里
单例模式(一种设计模式)仓库、服务和控制器是应该只实例化一次的类。实例化工作由一个名为 getControllers()
的函数完成,该函数负责返回应用程序中的所有控制器。
仓库和客户端程序首先实例化,然后被注入到服务中,这些服务接着被注入到控制器中——就像套娃那样。
这种方法让你能够完全掌控应用中资源创建的过程。你可以轻松处理创建控制器时所需的同步和异步操作。
一个控制器对应一个路由控制器然后通过路由器映射到API接口路由。每个控制器只处理一个路由请求。
好的,我们来看看用这种方法实际的服务是什么样的。
我们打算添加一个新的端点,负责返回某个客户的结构化信息。数据来自不同的来源渠道。
- 后端应用程序的数据库,存储了客户数据。
- 一个第三方产品API。
- 一个提供营销数据的第三方API。
以下是实现GET /customer/:id/info
端点所需的不同类。
我们可以做到
import { Customer, CustomerRegistry } from './registries/CustomerRegistry'
import { ProductCatalogClient } from './clients/ProductCatalogClient'
import { Logger } from './Logger'
import { MerchandisingProfile } from './types'
import { MerchandisingClient } from './clients/MerchandisingClient'
export class CustomerInfo {
readonly customerRegistry: CustomerRegistry
readonly productsClient: ProductCatalogClient
readonly merchandisingClient: MerchandisingClient
readonly formatter: InfoFormatter
readonly logger: Logger
constructor(
customerRegistry: CustomerRegistry,
productsClient: ProductCatalogClient,
merchandisingClient: MerchandisingClient,
formatter: InfoFormatter,
logger: Logger,
) {
this.customerRegistry = customerRegistry
this.productsClient = productsClient
this.merchandisingClient = merchandisingClient
this.formatter = formatter
this.logger = logger
}
public async getCustomerInfo(id: string): Promise<string> {
const customer = await this.customerRegistry.getCustomerById(id)
const { email } = customer
const products = await this.productsClient.getCustomerProducts(email)
const profile = await this.getMerchandisingProfile(customer)
return this.formatter.formatCustomerInfo(customer, products, profile)
}
private async getMerchandisingProfile({
id,
email,
fullname,
}: Customer): Promise<MerchandisingProfile | undefined> {
try {
return await this.merchandisingClient.getUserProfile(fullname, email)
} catch (e) {
this.logger.warn(`无法获取ID为 ${id} 的用户营销资料`)
}
}
}
哇哦!这看起来好整洁🤩!你有什么秘诀吗?
- 只有类型是通过模块导入的唯一实体。
- 类的 依赖 通过 构造函数参数 传递,并绑定到 类成员,因此它们在运行时可以直接使用于所有方法中。
- 方法 签名 保持 简明,因为依赖不需要作为参数传递,因此参数则专门用于与给定请求相关的特定上下文(例如,我们示例中的消费者 ID)。
就这样!另一个让你的同事们对你刮目相看并爱上你代码的妙招。
通过函数参数进行不一致的依赖注入这种事儿将一去不复返。
希望这篇文章你看了觉得还行,随时欢迎你分享你的想法,我们再深入聊聊。
别忘了:保持代码整洁 🧹。