Clean Node.js Architecture | 企业 Node.js + TypeScript

223 阅读7分钟

你听说过 "清洁架构 "吗?

也许你听说过它的不同名字......

清洁架构洋葱架构端口和适配器六边形架构、分层架构、DCI(数据、上下文和交互)等等。

它们在实现上都有一点不同,但对于我们的理解:它们都意味着同样的事情。

架构层面上的关注点分离

我第一次发现这个术语是在读罗伯特-C-马丁(Bob叔叔)的《清洁架构》时(尽管有一些负面评论,但这本书实际上是一本令人难以置信的读物,我强烈建议你去看看它)。

在阅读了他的书并花了一些时间学习SOLID原则后,我不仅享受到了我的代码的灵活性和可测试性得到了改善,而且我在使用TypeScript和Node.js解决复杂的软件开发问题时变得更加自信

在这篇文章中,我将介绍。

  • 清晰的架构如何分离你的代码的关注点
  • 它如何让你写出可测试的代码
  • 它如何使你写出灵活的代码

了解清洁架构

政策与细节

当我们在写代码时,在任何时候,我们不是在写策略就是在写细节

政策是指我们指定什么应该发生,以及什么时候发生。

政策主要涉及业务逻辑、规则和我们正在编码的领域中存在的抽象概念。

细节是当我们指定政策 如何发生。

细节实际上是执行政策。细节是政策的实现。

一个简单的方法来弄清楚你写的代码是细节还是策略,就是问自己。

  • 这段代码是在我的领域中强制执行某项规则?
  • 还是说这段代码只是让某些东西工作

出于这个原因:框架(如Nest.jsExpress.js)、npm库(如lodashRxJsRedux)是细节。

再次强调。

清洁架构的最终目标是在架构层面上分离政策与细节。

所以,让我们看看这看起来像什么。

分层架构

那些小的半圆是为了表示编写接口(在策略层面),由细节层面来实现

这张图是我发现的所有其他图表的一种简化。不仅仅是这两层(阅读"用清洁架构组织应用程序逻辑"以获得更详细的描述)。但对于我们对这一概念的理解,像这样思考一个干净的架构要容易得多。

那么这意味着什么呢?

在一层(领域),我们有所有重要的东西:实体业务逻辑规则事件。这是我们软件中不可替代的东西,我们不能用另一个库或框架来代替。

另一层(infra)包含了在领域层中实际旋转执行的所有代码。

你会记得这是MVC中最大的挑战,弄清楚 "M "应该做什么以及如何做。那么,这就是它。M"=领域层。

下面是一个RESTful HTTP调用如何导致代码在我们整个架构中被执行的例子。

关于依赖关系的方向,这里有一个模式。

依赖性规则

在鲍勃叔叔的书中,他描述了依赖性规则

该规则规定,外圈声明的东西在代码中不得被内圈提及。

在其他图中,还有很多层。该规则仍然适用。

这意味着,代码只能指向内部。

领域层代码不能依赖基础设施层代码。

但基础设施层代码可以依赖领域层代码(因为它向内)。

当我们遵循这个规则时,我们基本上是在遵循SOLID原则中的依赖反转规则。


端口和适配器的方式来思考清洁的架构

端口和适配器的思考方法是:接口抽象类端口具体类(即:实现)是适配器

让我们把它形象化。

假设我设计了一个IEmailService 接口。它规定了电子邮件服务可以做的所有事情。但它实际上并没有具体实现这些东西。

export interface IEmailService {
  sendMail(mail: IMail): Promise<IMailTransmissionResult>; 
}

这是我的小端口

假设我只是在为一些依赖IEmailService 的代码布线。

class EmailNotificationsService implements IHandle<AccountVerifiedEvent> {
  private emailService: IEmailService;
  constructor (emailService: IEmailService) {
    DomainEvents.register(this.onAccountVerifiedEvent, AccountVerifiedEvent.name)
  }

  private onAccountVerifiedEvent (event: AccountVerifiedEvent): void {
    emailService.sendMail({
      to: event.user.email,
      from: 'me@khalilstemmler.com',
      message: "You're in, my dude"
    })
  }
}

因为我指的是策略,所以剩下的事情就是创建实现(细节)。

// We can define several valid implementations.
// This infra-layer code relies on the Domain layer email service.
class MailchimpEmailService implements IEmailService {
  sendMail(mail: IMail): Promise<IMailTransmissionResult> {
    // alg
  }
}

class SendGridEmailService implements IEmailService {
  sendMail(mail: IMail): Promise<IMailTransmissionResult> {
    // alg
  }
}

class MailgunEmailService implements IEmailService {
  sendMail(mail: IMail): Promise<IMailTransmissionResult> {
    // alg
  }
}

当我去连接这个东西的时候,我现在有几个选项可用。

// index.js
import { EmailNotificationsService } from './services/EmailNotificationsService'
import { MailchimpEmailService } from './infra/services/MailchimpEmailService'
import { SendGridEmailService } from './infra/services/SendGridEmailService'
import { MailgunEmailService } from './infra/services/MailgunEmailService'

const mailChimpService = new MailchimpEmailService();
const sendGridService = new SendGridEmailService();
const mailgunService = new MailgunEmailService();

// I can pass in any of these since they are all valid IEmailServices
new EmailNotificationsService(mailChimpService) 

看!这个端口适合适配器❤️。


希望我们开始看到这可以使我们的代码更加可测试灵活

代码是可测试的

如果你遵循依赖性规则,域层代码有0个依赖性。

你知道这意味着什么吗?

我们实际上可以测试它

下次你在写代码的时候,可以这样想一下...

在你在一些类上的工作走得太远之前,问问自己。"我可以模拟我刚刚写的东西吗?"

如果你遵循SOLID,并参考了接口抽象类,答案将是肯定的

如果你指的是凝结物,那么为它写测试就会有相当大的挑战性。这是由耦合引起的。

代码是灵活的

当我们把策略和细节之间的关注点分开时,我们创建了一个明确的关系,我们知道如何去处理。

如果我们改变策略,我们最终可能会影响到细节(因为细节取决于策略)。

但是,如果我们改变了细节,就不应该影响政策,因为政策并不依赖于细节。

这种关注点的分离,加上对SOLID原则的坚持,使得改变代码变得更加容易。

测试?

我们能够确定改变代码的唯一方法是为它编写测试。

领域代码是非常容易测试的(因为它没有依赖性),并且指的是抽象的。我们可以通过用我们的模拟类来实现接口,真的很容易为事物创建模拟。

基础设施层的代码在测试上更具挑战性(也更慢),因为它有依赖性(网络服务器、缓存、键值对象存储如Redis等)。

太干净了?

你构建的软件越复杂,越需要健壮,你就越需要在其中建立灵活的机制。

例如:如果你正在编写一个快速的Node.js脚本来定期地抓取一个特定的网页,这样你就可以实现自动化,不要花太多的时间来试图使你的代码变得简洁。

但是,如果你正在构建一个网页搜刮器,需要知道如何搜刮世界上所有100个最受欢迎的招聘网站,那么你可能要考虑将其编码为灵活性。

// Define the abstraction to implement the algorithms
abstract class BaseScraper {
  constructor () {
    this.puppeteer = new Puppeteer();
  }
  abstract getNumberPages (): Promise<number>;
  abstract getJobTitle (): Promise<HTML>;
  abstract getJobDescription(): Promise<HTML>;
  abstract getJobPaymentDetails(): Promise<HMTL>;
}

// Implement the algorithms
class LinkedInScraper extends BaseScraper {
  constructor () {
    super();
  }

  getNumberPages (): Promise<number> {
    // implement algorithm
    this.puppeteer...
  }

  getJobTitle (): Promise<HTML> {
    // implement algorithm
  }

  getJobDescription(): Promise<HTML> {
    // implement algorithm
    
  }

  getJobPaymentDetails(): Promise<HMTL> {
    // implement algorithm
  }
}

class GlassdoorScraper extends BaseScraper {
  constructor () {
    super();
  }
  
  getNumberPages (): Promise<number> {
    // implement algorithm
    this.puppeteer...
  }

  getJobTitle (): Promise<HTML> {
    // implement algorithm
  }

  getJobDescription(): Promise<HTML> {
    // implement algorithm
    
  }

  getJobPaymentDetails(): Promise<HMTL> {
    // implement algorithm
  }
}