您的代码就是堡垒:为什么六边形架构能拯救您的JavaScript项目
\n\n您是否曾遇到过这样的情况:更换数据库或替换外部API变成了一场重写半个项目的噩梦?或者,您编写的单元测试更像是集成测试,因为业务逻辑与Express或MongoDB紧密耦合?如果是这样——欢迎来到六边形架构(端口与适配器)的世界。
\n\n这不仅仅是Alistair Cockburn书中的时髦模式。这是一种让您的JavaScript代码变得健壮、可测试且独立于框架的实用方法。在本文中,我们不会进行枯燥的讲解。我们将以一个真实任务(订单管理系统)为例,逐步按照JavaScript中的六边形架构原则构建它。您将看到业务逻辑如何保持纯净,而所有“噪音”(数据库、HTTP请求、队列)都被隔离在外。
\n\n准备好了吗?开始吧!
\n\n什么是六边形架构(端口与适配器是什么关系)?
\n\n想象一艘宇宙飞船的闸门。内部是控制台(您的业务逻辑)。外部是无数不同的星球(数据库、Web框架、外部服务)。闸门就是端口(接口)。而让您在特定星球上呼吸的宇航服就是适配器。
\n\n核心思想是:应用程序核心不应该知道谁在以及如何访问它。它只声明“端口”(接口)。而具体的实现(适配器)从外部接入。这带来了三大超能力:
\n\n- \n
- 业务逻辑隔离——更换数据库时无需触及折扣计算代码。 \n
- 可测试性——无需启动Docker和真实数据库即可测试逻辑。 \n
- 框架无关性——只需编写一个新适配器,即可从Express迁移到Fastify。 \n
让我们进入代码部分。
\n\n项目设置与文件夹结构
\n\n首先,创建一个简单的结构。我们将使用纯TypeScript(或带JSDoc的JavaScript——随您方便)。我们的目标是构建一个订单管理系统。
\n\nsrc/\n├── core/\n│ ├── domain/ # 实体和值对象\n│ ├── ports/ # 接口(端口)\n│ └── services/ # 业务逻辑(用例)\n├── adapters/\n│ ├── primary/ # 调用我们核心的适配器(HTTP、CLI)\n│ └── secondary/ # 核心调用的适配器(数据库、外部API)\n└── app.js # 入口点(组装所有组件)\n\n\n步骤1. 核心:领域实体与端口
\n\n从最重要的部分开始——不依赖任何东西的部分。创建Order(订单)实体。
core/domain/Order.js
\n\nexport class Order {\n constructor({ id, userId, items, status = 'pending' }) {\n this.id = id;\n this.userId = userId;\n this.items = items; // 数组 { productId, quantity, price }\n this.status = status;\n this.total = this.calculateTotal();\n }\n\n calculateTotal() {\n return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);\n }\n\n approve() {\n if (this.status !== 'pending') {\n throw new Error('Only pending orders can be approved');\n }\n this.status = 'approved';\n }\n}\n\n\n现在定义端口——核心期望从外部世界获得的接口。
\n\ncore/ports/OrderRepository.js
\n\n// 这是一个端口(接口)。我们不在这里实现它。\nexport class OrderRepository {\n async save(order) { throw new Error('Not implemented'); }\n async findById(id) { throw new Error('Not implemented'); }\n}\n\n\ncore/ports/EmailService.js
\n\nexport class EmailService {\n async sendOrderConfirmation(order) { throw new Err