TypeScript中的DDD实践(六):模块

图片

TypeScript 中的 DDD 之旅将我们引向负责项目结构的模式 —— 模块

当我们想到设计模式时,我们会想到一些代码结构,这些代码结构或多或少定义了软件开发中的一些良好实践。

很多人不会以常规的方式将模块模式视为一种实际的模式,这是可以理解的,因为有些人可能更倾向于将模块视为项目结构而不是模式。

当我们考虑 NodeJS 模块时,会遇到更多的麻烦。它们是用 JavaScript 和 TypeScript 编写的代码的耦合集合,一起版本化和发布。

问题是如何将这个故事与领域驱动设计中的模块模式联系起来。这样做是否有意义,或者我们可以没有这个模式而继续生活?

与域驱动设计中的其他模式一样,模块在软件开发中的日常使用中也有例子,当然,这种使用可能与其他语言不同,但主要思想仍然存在。

让我们来研究一下这个想法。

结构

图片


在 TypeScript 和 Javascript 中,我们把代码分组到文件中,然后把它们放到一些文件夹中。这些文件夹会自动成为一些包,然后我们可以导入到其他文件中。

从逻辑上讲,在这种情况下,有一种使用某些文件夹结构来帮助我们在代表其实际用途的文件夹内对文件进行分组的感觉,比如模型、服务、存储库......

文件
./src/customer/domain/model/customer.entity.ts

export class Customer {  constructor(    private readonly id: number,    private firstName: string,    private lastName: string,    private email: string) {}}


文件夹
./src/customer/domain/port/customer.repository.ts

import { Customer } from "../model/customer.entity";
export interface ICustomerRepository {  getCustomer(id: number): Promise<Customer>;}

在上面的例子中,我们可以看到文件夹命名的一些方向,将我们的源代码分成模块,模块分成层,层分成一些更紧密的组。

现在我们已经对前一个例子中的 DDD Module 有了一点了解,在这里,Module 是  customer  包,以及它所有的子文件夹。

project|--src   |--customer      |--infrastructure         |--provider            |--customer.provides.ts         |--adapter            |--database               |--customer.repository.ts               |--customer.dao.ts            |--mock               |--customer.repository.ts      |--presentation         |--controller            |--customer.controller.ts      |--application         |--query            |--customer.byId.handler.ts            |--customer.byId.query.ts         |--command            |--customer.create.handler.ts            |--customer.create.command.ts      |--domain              |--service            |--customer.factory.ts            |--port            |--customer.repository.ts            |--model            |--customer.entity.ts       |--customer.module.ts   |--access      |--...   |--shopping      |--...   |--...|--packages.json|--...

上面的文件夹结构代表了我最喜欢的项目结构,它在 TypeScript 和 JavaScript 中实现了域驱动设计。

在我的项目中,每个模块最多有四个基本文件夹:
infrastructure , presentation , application ,和 domain 。
正如你所看到的,我喜欢遵循分层架构的原则。

这里我把 infrastructure 文件夹放在了顶部,这是因为遵循了Bob叔叔的依赖反转原则,来自 infrastructure 层的低级服务实现了来自其他层的高级接口。

使用这种方法,我确保我将端口定义为domain层上的 
CustomerRepository 接口。

实际的实现是在 infrastructure 层,或者一个适配器,它可以是多个适配器,比如 
CustomerDBRepository 或 CustomerMockRepository

CustomerMockRepository 的示例​​​​​​​

import { Customer } from '../../../domain/model/customer.entity'import { ICustomerRepository } from '../../../domain/port/customer.repository'
export class CustomerMockRepository implements ICustomerRepository {  public async getCustomer(id: number): Promise<Customer> {    const customer = new Customer(      // some dummy data    );
    return customer;  }}

CustomerDBRepository 的示例​​​​​​​

import { Repository, EntityRepository } from 'typeorm'import { CustomerDAO } from '../customer.dao'import { Customer } from '../../../domain/model/customer.entity'import { ICustomerRepository } from '../../../domain/port/question.repository'
@EntityRepository(CustomerDAO)export class CustomerDBRepository extends Repository<CustomerDAO> implements ICustomerRepository {    constructor(private readonly manager: EntityManager) {}
  public async getCustomer(id: number): Promise<Customer> {    const customerDao = await this.manager.findOne(CustomerDAO, id);
    return customerDao.toEntity;  }
}

关于端口和适配器的故事并不新鲜,它属于六边形架构的原则,是我在设计DDD模块时使用的第二个原则,对我来说,这是至关重要的一个原则。

回到模块内部的文件夹结构,每个层都知道下面所有层的一切,而没有人知道它们的任何事情,因此,infrastructure 层可以依赖所有层,而 domain 层则不依赖任何层。

在 infrastructure 层的下面是 presentation 层。我们也可以称它为 interface 层,但 presentation 似乎更好。最后,在 presentation 和 domain 之间,是 application 层。

这种分层的好处在于它可以帮助我们避免循环依赖,通过遵循这些依赖分层规则和方向,我们可以避免痛苦的代码重构。

最后,你注意到了  domain  层中的一些文件夹: model  、 service  等。我偶尔会放置它们,以使我的包尽可能简单。

有时,我使用  domain  中的子结构来避免循环依赖,我选择在底部使用  model  ,在顶部使用  service  ,但这取决于个人的喜好。

逻辑集群

图片

DDD 模块不仅仅是一些文件和文件夹的组合,这些文件和文件夹中的代码必须代表某种内聚结构。

不仅如此,两个不同的模块应该松散耦合,它们之间的依赖性最小。​​​​​​​

project|--src   |--customer      |--infrastructure         |--...      |--presentation         |--...      |--application         |--...      |--domain          |--...          |--customer.module.ts   |--access      |--infrastructure         |--...      |--presentation         |--...      |--application         |--...      |--domain         |--...        |--access.module.ts   |--shopping      |--infrastructure         |--...      |--presentation         |--...      |--application         |--...      |--domain         |--...        |--shopping.module.ts   |--...|--packages.json|--...

上面的文件夹结构是一个简单的DDD模块示例,其中我们有三个模块(可能更多),分别是 
access,   shopping,  和 customer ,它们都有自己的层和子层。

 access  模块与授权和注册过程相关,它包含了处理会话中一个  User  的全部逻辑,此外,它还持有每个  User  的访问权限,并决定它们是否可以访问特定对象。

 customer 模块包含有关 Customers 及其 Addresses 的信息。尽管它可能看起来与 User 相同,但它代表了制造 Orders 的业务实体,其中 User 是会话中的实体。

另外,一个 User 可以有多个 Customers 进行交付,就像我们在许多平台上已经做到的那样。

最后, shopping  模块是一个集群,用于完成逻辑,包括  Basket  的创建、保持会话状态以及进一步创建  Orders 。这个  shopping  模块看起来比其他两个模块更复杂,而且确实依赖于其他两个模块。

而且,就像层一样,我们也应该跟踪模块之间的依赖关系,并确保它们是单向的。

模块依赖关系

如上图所示,shopping 模块使用  customer  模块来查找  Order  的所有者是谁,然后使用  Address  来定义交付。

它还依赖于  access  模块来检查特定的  Baskets  和  Items  的访问权限。

 customer  模块仅依赖于  access  模块。它提供到会话中的  User  的连接和一个指定的  Customers  列表,以决定向谁发送  Order 。

 customer  和  shopping  可以一起定义一个 Bounded Context。单个模块不需要表示一个 Bounded Context。我喜欢将一个 Bounded Context 分成多个模块。

 access  模块看起来可能像是一个不同的 Bounded Context 的候选者,将来,我们可以考虑把它放在其他地方。

虽然  shopping  和  customer  看起来是耦合在一起的,但在我们的应用程序中,我们决定将它们分开。原因是作为一个 Customer,我们可以独立于  Orders  做许多不同的事情。

我们可以更改我们的 Addresses ,查看我们的历史记录,跟踪我们的交付,并联系客户支持。 Customer 细节的更改不应影响一个订单。同样,一个 Order 的更改不应影响 Customer 。我们可以独立地与它们合作。

命名


谈论命名可能看起来很奇怪,但不幸的是,它不是。根据我的经验,我已经看到过可怕的DDD模块的名称,我创建过更糟糕的。

我们的模块应该有来自真实商业世界的名称,它应该是泛在语言的一部分,一些术语属于商业和软件开发世界,它描述的是同一件事,它应该是业务逻辑集群的唯一名称。​​​​​​​

project|--...|--src   |--shoppingAndCustomer      |--...   |--utils      |--...   |--events      |--...   |--strategy      |--...   |--...|--...

上面的例子包含了许多不同的不好的名字。我总是避免在模块的名字中使用“and”这个词,就像这里 shoppingAndCustomer 。如果我不能避免这个词“and”,那么我可能正在处理两个独立的模块。

在软件开发中,“utils”这个词是最糟糕的名字。我不能忍受它作为类名、文件名、函数名、包名或模块名。名字“Garbage Collector”可能更好,因为它最好地描述了存储在 utils 模块中的代码。

一个模块包含了来自各个地方的小部分也是没有用的, events  模块就是这样一个例子——它包含了来自整个应用的 Domain Events。

用某种设计模式来命名模块也不是什么好习惯,比如 strategy 模块,可能我们应该在应用的很多地方使用策略模式,所以创建多个 strategy 模块没有意义。

依赖注入


你可能注意到第一个项目结构在每个 DDD 模块的根目录中引入了单独的 TypeScript 文件,我总是将它们的名字以  module.ts  结尾。

这些文件是我在模块中定义依赖和为端口定义适配器的地方,在很多情况下,我编写在 NestJS 应用中使用的模块和提供商。

客户的供应商​​​​​​​

import { CustomerDBRepository } from '../adapter/repository/customer.repository'import { CustomerMockRepository } from '../adapter/mock/customer.repository'
export const CUSTOMER_REPOSITORY_TOKEN = 'customerRepository'
export const customerProvider = {  provide: CUSTOMER_REPOSITORY_TOKEN,  useFactory: () => {    if (process.env.USE_MOCK === 'true') {      return new CustomerMockRepository();    }    return new CustomerDBRepository();  },};


客户模块​​​​​​​

import { Module } from '@nestjs/common'import { CustomerController } from './presentation/controller/customer.controller'import { customerProvider } from "./infrastructure/provider/customer.provider";import { CqrsModule } from '@nestjs/cqrs'import { CustomerByIdHandler } from './application/query/customer.byId.handler'
@Module({  imports: [CqrsModule],  controllers: [CustomerController],  providers: [customerProvider, CustomerByIdHandler],})export class CustomerModule {}

在上面的例子中,我创建了  CustomerModule  类,在初始化期间,它接受配置,定义它是否应该依赖于数据库或一些 CustomerRepository 的模拟实现,之后,所有其他模块都可以使用这个来获取它们的依赖。

结论

DDD模块是我们代码的逻辑集群。它将许多结构耦合在一个内聚组中,共享一些业务规则。在模块内部,我们可以引入不同的层。



TypeScript领域驱动设计(DDD)系列:

1. TypeScript中的实用领域驱动设计(DDD):为什么重要?

2. TypeScript 中 DDD 的实践:值对象

3. 在TypeScript中实践DDD(领域驱动设计):实体

4. 在TypeScript中实践DDD(领域驱动设计):域服务

5. TypeScript中的DDD实践(五):域事件

欢迎关注公众号:文本魔术,了解更多

  • 18
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
该资源内项目源码是个人的课程设计、毕业设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 该资源内项目源码是个人的课程设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。
该资源内项目源码是个人的课程设计、毕业设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 该资源内项目源码是个人的课程设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值