你听说过 "清洁架构 "吗?
也许你听说过它的不同名字......
清洁架构、洋葱架构、端口和适配器、六边形架构、分层架构、DCI(数据、上下文和交互)等等。
它们在实现上都有一点不同,但对于我们的理解:它们都意味着同样的事情。
架构层面上的关注点分离
我第一次发现这个术语是在读罗伯特-C-马丁(Bob叔叔)的《清洁架构》时(尽管有一些负面评论,但这本书实际上是一本令人难以置信的读物,我强烈建议你去看看它)。
在阅读了他的书并花了一些时间学习SOLID原则后,我不仅享受到了我的代码的灵活性和可测试性得到了改善,而且我在使用TypeScript和Node.js解决复杂的软件开发问题时变得更加自信。
在这篇文章中,我将介绍。
- 清晰的架构如何分离你的代码的关注点
- 它如何让你写出可测试的代码
- 它如何使你写出灵活的代码
了解清洁架构
政策与细节
当我们在写代码时,在任何时候,我们不是在写策略就是在写细节。
政策是指我们指定什么应该发生,以及什么时候发生。
政策主要涉及业务逻辑、规则和我们正在编码的领域中存在的抽象概念。
细节是当我们指定政策 如何发生。
细节实际上是执行政策。细节是政策的实现。
一个简单的方法来弄清楚你写的代码是细节还是策略,就是问自己。
- 这段代码是在我的领域中强制执行某项规则?
- 还是说这段代码只是让某些东西工作
出于这个原因:框架(如Nest.js和Express.js)、npm库(如lodash、RxJs或Redux)是细节。
再次强调。
清洁架构的最终目标是在架构层面上分离政策与细节。
所以,让我们看看这看起来像什么。
分层架构
那些小的半圆是为了表示编写接口(在策略层面),由细节层面来实现。
这张图是我发现的所有其他图表的一种简化。不仅仅是这两层(阅读"用清洁架构组织应用程序逻辑"以获得更详细的描述)。但对于我们对这一概念的理解,像这样思考一个干净的架构要容易得多。
那么这意味着什么呢?
在一层(领域),我们有所有重要的东西:实体、业务逻辑、规则和事件。这是我们软件中不可替代的东西,我们不能用另一个库或框架来代替。
另一层(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
}
}
