那些年我们写过的“面条代码”

那些年我们写过的“面条代码”
你一定经历过这样的噩梦系统最初用 MySQL 存储数据后来为了性能要迁移到 MongoDB。结果你发现业务代码里密密麻麻全是对 MySQL 驱动的直接调用。或者老板突发奇想要求把原本的 Web 页面功能原封不动地搬到一个新的命令行工具CLI里你却发现业务逻辑和 HTTP 的Request/Response对象死死绑定在一起。“牵一发而动全身”修改一行代码整个系统崩溃。这是因为我们的核心业务逻辑被外部框架、数据库和 UI 强行“绑架”了。解决方案这时候六边形架构Hexagonal Architecture闪亮登场。它的核心理念极其简单将核心业务逻辑与外部依赖彻底隔离。它能让你的业务代码变得像插座一样通用无论外接的是哪种数据库或哪个前端框架核心系统都能从容应对。概念拆解餐厅后厨的生存哲学 (The What Why)生活化类比米其林餐厅的运作别被“六边形”这个高大上的名字吓到它其实也被称为端口与适配器模式Ports and Adapters。我们用“去餐厅点餐”来理解它想象一家顶级的米其林餐厅它的核心竞争力是“后厨大厨的烹饪手艺”核心业务逻辑。输入端Driving大厨根本不在乎客人是通过服务员点餐、通过美团外卖下单还是打电话预定。大厨只认一样东西标准化的点餐单输入端口 Input Port。服务员和外卖App在这里就是输入适配器Input Adapter负责把各种乱七八糟的请求翻译成大厨能看懂的点餐单。输出端Driven大厨做菜需要土豆。他不会自己跑去菜市场买他只会对采购员下达指令“给我拿两个土豆”输出端口 Output Port。至于采购员是从门口超市买的还是从远洋货轮上空运的输出适配器 Output Adapter如 MySQL、Redis、第三方 API大厨毫不关心。工作流图解洋葱般的结构如果画个图六边形架构就像一个洋葱分为内、中、外三层最内层Domain纯粹的业务实体和规则大厨的手艺。这里没有任何外部框架的代码。中间层Ports接口定义层点餐单和采购单。它规定了外部如何与核心交互以及核心如何向外部要数据。最外层Adapters具体的实现服务员、外卖App、采购员。比如 Spring Boot 控制器、REST API、MyBatis Mapper。核心原则依赖只能从外向内外层可以调用内层的接口但内层绝对不能知道外层的任何细节。动手实战用 TypeScript 烤一个“六边形”MVP (The How)让我们用 TypeScript 写一个极简的“创建用户”功能感受一下六边形架构的魅力。1. 核心领域与端口Domain Ports首先我们定义核心逻辑和它需要的“契约”。这部分代码绝对不能引入任何外部库比如express或mongoose。TypeScript// 1. 领域模型 (Domain Model) - 纯纯的业务对象 export class User { constructor(public readonly id: string, public name: string, public email: string) {} } // 2. 输出端口 (Output Port) - 核心业务向外要数据的“采购单” export interface UserRepositoryPort { save(user: User): Promisevoid; findByEmail(email: string): PromiseUser | null; } // 3. 输入端口 (Input Port) - 外部调用核心业务的“点餐单” export interface CreateUserUseCase { execute(name: string, email: string): PromiseUser; }2. 应用服务Application Service接下来实现核心业务逻辑。它实现了输入端口并调用输出端口。TypeScript// 4. 核心业务逻辑实现 export class UserService implements CreateUserUseCase { // 依赖注入我不在乎你传给我的是 MySQL 还是 MongoDB只要实现了接口就行 constructor(private readonly userRepository: UserRepositoryPort) {} async execute(name: string, email: string): PromiseUser { const existingUser await this.userRepository.findByEmail(email); if (existingUser) { throw new Error(邮箱已被注册); // 纯粹的业务异常 } const newUser new User(Date.now().toString(), name, email); await this.userRepository.save(newUser); // 调用输出端口 return newUser; } }3. 适配器层Adapters最后我们编写外围代码连接真实的世界。TypeScript// 5. 输出适配器 (Output Adapter) - 真正连接数据库的地方 // 假设这里用的是内存数据库随时可以换成 MongoUserRepository export class InMemoryUserRepository implements UserRepositoryPort { private users: User[] []; async save(user: User): Promisevoid { this.users.push(user); console.log([Database] 用户 ${user.name} 已保存到内存数据库。); } async findByEmail(email: string): PromiseUser | null { return this.users.find(u u.email email) || null; } } // 6. 组装运行 (类似框架的 Controller / CLI 入口) async function main() { // 装配阶段将适配器插入端口 const repository new InMemoryUserRepository(); const userService new UserService(repository); // 模拟一个 HTTP 请求进来 console.log(-- 收到前端请求创建用户张三); try { const user await userService.execute(张三, zhangsanexample.com); console.log(-- 响应前端创建成功, user); } catch (error) { console.error(-- 响应前端创建失败, error.message); } } main();代码解析为什么这么写你会发现UserService里没有任何数据库的影子。如果明天老板说要把用户数据存到 Redis你只需要新建一个RedisUserRepository实现UserRepositoryPort接口然后在组装阶段替换掉InMemoryUserRepository即可。核心业务代码一行都不用改进阶深潜新手防坑指南 (Deep Dive)常见陷阱