设计模式最容易学错的方式,是先记住一组名字,再试图给代码贴标签。
真正写代码时,更有用的问题是:
- 哪些接口不兼容?
- 哪些逻辑正在重复?
- 哪些部分未来最可能变化?
模式的作用不是让代码显得高级,而是把变化限制在局部,让稳定部分保持简单。
Hono 的 AWS Lambda Adapter 是一个很小但完整的例子。
先看两个不兼容的接口
Hono 应用提供的是 Fetch 风格接口:
app.fetch(request: Request): Response | Promise<Response>AWS Lambda Runtime 调用的则是:
handler(event, context): LambdaResult | Promise<LambdaResult>用户通常这样连接两者:
import { Hono } from 'hono'
import { handle } from 'hono/aws-lambda'
const app = new Hono()
app.get('/', (c) => c.text('Hello'))
export const handler = handle(app)这里有两个阶段:
模块初始化:
handle(app) 返回一个 Lambda handler
请求到达:
Lambda Runtime 调用 handler(event, context)简化后的实现是:
function handle(app) {
return async function handler(event, context) {
const processor = getProcessor(event)
const request = processor.createRequest(event)
const response = await app.fetch(request, { event, context })
return processor.createResult(event, response)
}
}这段代码里同时出现了几种模式,但每一种都对应一个具体问题。
Adapter:外部接口不应该污染内部核心
Lambda Event 和 Fetch Request 的结构不同,Lambda Result 和 Fetch Response 也不同:
Lambda Event -> Fetch Request
Lambda Result <- Fetch ResponseAdapter 负责这组双向转换。
使用 Adapter 的典型信号是:
- 正在对接外部系统。
- 外部格式与内部模型不同。
- 不希望核心业务理解多个平台的细节。
- 更换运行时、供应商或协议时,希望只修改边界层。
同样的结构还会出现在:
Node IncomingMessage -> 内部 Request
第三方支付回调 -> 内部 PaymentEvent
数据库行数据 -> Domain Model
不同 LLM 厂商响应 -> 统一 CompletionAdapter 的关键不是“包了一层”,而是建立稳定的内部语言。
Strategy:同一件事存在多种实现
AWS Lambda 不只有一种事件结构。API Gateway V1、V2、Application Load Balancer 和 VPC Lattice 都需要提供 method、path、headers、query 和 body,但字段位置不同。
如果全部写进 handle(),代码很容易变成:
if (isV1(event)) {
// V1 转换
} else if (isV2(event)) {
// V2 转换
} else if (isALB(event)) {
// ALB 转换
}Hono 把每种转换封装成 processor:
const processor = getProcessor(event)
const request = processor.createRequest(event)
const result = processor.createResult(event, response)这些 processor 做的是同一件事,但实现不同:
EventV1Processor
EventV2Processor
ALBProcessor
LatticeV2ProcessorStrategy 的信号是:
- 同一目标有多种算法或平台实现。
- 调用方不应该关心具体实现。
if/switch正随实现数量增长。- 未来还会增加新变体。
Factory:把选择逻辑集中起来
Strategy 解决“怎么做”,但还需要决定“这次用哪个”。
function getProcessor(event) {
if (isProxyEventALB(event)) {
return albProcessor
}
if (isProxyEventV2(event)) {
return v2Processor
}
if (isLatticeEventV2(event)) {
return latticeV2Processor
}
return v1Processor
}getProcessor() 不处理请求,只集中选择 processor。它更接近简单工厂或策略选择器。
Factory 的信号是:
- 创建或选择对象的逻辑开始复杂。
- 同样的选择判断散落在多个调用点。
- 调用方只想拿到可用对象,不想理解选择过程。
Factory 和 Strategy 经常一起出现:
Factory 负责选哪个
Strategy 负责怎么做Template Method:流程稳定,少数步骤变化
不同 Lambda Event 的字段位置不同,但创建标准 Request 的大流程基本稳定:
取得 query
取得 domain
取得 path
拼接 URL
取得 headers
取得 method
处理 body
创建 Request因此可以把公共流程放在基类,把变化步骤交给子类:
abstract class EventProcessor {
createRequest(event): Request {
const query = this.getQueryString(event)
const domain = this.getDomainName(event)
const path = this.getPath(event)
const method = this.getMethod(event)
const headers = this.getHeaders(event)
return new Request(buildUrl(domain, path, query), {
method,
headers,
body: this.getBody(event),
})
}
protected abstract getPath(event): string
protected abstract getMethod(event): string
protected abstract getQueryString(event): string
protected abstract getHeaders(event): Headers
}Template Method 的信号是:
- 多个实现共享相同大流程。
- 差异集中在少数步骤。
- 需要保证所有实现遵循同一执行顺序。
它把“流程骨架”和“可替换步骤”分开。
为什么这不是 Proxy
Proxy 的重点是控制对真实对象的访问,例如权限、缓存、延迟加载或远程调用:
调用方 -> Proxy -> 真实对象Proxy 和真实对象通常提供相同接口。
Hono Lambda Adapter 的重点却是接口转换:
Lambda Event -> Request
Response -> Lambda ResultgetProcessor() 负责选择转换策略,handle(app) 负责连接两个不同接口。它们不是为了控制对 app 的访问,因此不应仅因为“中间包了一层”就叫 Proxy。
一个更实用的判断顺序
写代码时,可以按下面的顺序判断:
- 先直接实现,让真实需求出现。
- 找变化点:哪里会因平台、协议或数据类型改变?
- 找重复流程:哪些步骤在多个实现中反复出现?
- 找边界:哪些外部格式不应该进入内部核心?
- 再决定是否引入模式。
可以用这个表做快速检查:
| 代码现象 | 更可能需要 |
|---|---|
| 外部接口和内部接口不兼容 | Adapter |
| 同一件事有多种实现 | Strategy |
| 需要集中选择实现 | Factory |
| 大流程稳定、少数步骤变化 | Template Method |
| 需要控制对真实对象的访问 | Proxy |
模式最终保护的是变化
Hono 核心只理解 Fetch 标准:
app.fetch(request)Node、Lambda、Vercel 等平台的差异被限制在各自 Adapter 中。Lambda 内部又把不同 Event 格式拆成可选择的 processor,并复用稳定的 Request 构建流程。
因此,这个例子真正值得记住的不是四个模式名称,而是四个判断:
接口不兼容,需要转换
同一目标存在多种实现
选择逻辑需要集中
公共流程已经稳定设计模式应当从这些信号中长出来。它不是编码前必须完成的仪式,而是复杂度和变化方向变得明确后,对代码边界做出的回应。