Engineering

什么时候该用设计模式:从 Hono Lambda Adapter 看变化点

设计模式不是先背名字再套代码。Hono 的 Lambda Adapter 展示了一个更实用的判断顺序:先找接口不兼容、重复流程和变化方向,再决定是否需要 Adapter、Strategy、Factory 或 Template Method。

设计模式最容易学错的方式,是先记住一组名字,再试图给代码贴标签。

真正写代码时,更有用的问题是:

  1. 哪些接口不兼容?
  2. 哪些逻辑正在重复?
  3. 哪些部分未来最可能变化?

模式的作用不是让代码显得高级,而是把变化限制在局部,让稳定部分保持简单。

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 Response

Adapter 负责这组双向转换。

使用 Adapter 的典型信号是:

  • 正在对接外部系统。
  • 外部格式与内部模型不同。
  • 不希望核心业务理解多个平台的细节。
  • 更换运行时、供应商或协议时,希望只修改边界层。

同样的结构还会出现在:

Node IncomingMessage -> 内部 Request
第三方支付回调       -> 内部 PaymentEvent
数据库行数据         -> Domain Model
不同 LLM 厂商响应    -> 统一 Completion

Adapter 的关键不是“包了一层”,而是建立稳定的内部语言。

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
LatticeV2Processor

Strategy 的信号是:

  • 同一目标有多种算法或平台实现。
  • 调用方不应该关心具体实现。
  • 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 Result

getProcessor() 负责选择转换策略,handle(app) 负责连接两个不同接口。它们不是为了控制对 app 的访问,因此不应仅因为“中间包了一层”就叫 Proxy。

一个更实用的判断顺序

写代码时,可以按下面的顺序判断:

  1. 先直接实现,让真实需求出现。
  2. 找变化点:哪里会因平台、协议或数据类型改变?
  3. 找重复流程:哪些步骤在多个实现中反复出现?
  4. 找边界:哪些外部格式不应该进入内部核心?
  5. 再决定是否引入模式。

可以用这个表做快速检查:

代码现象更可能需要
外部接口和内部接口不兼容Adapter
同一件事有多种实现Strategy
需要集中选择实现Factory
大流程稳定、少数步骤变化Template Method
需要控制对真实对象的访问Proxy

模式最终保护的是变化

Hono 核心只理解 Fetch 标准:

app.fetch(request)

Node、Lambda、Vercel 等平台的差异被限制在各自 Adapter 中。Lambda 内部又把不同 Event 格式拆成可选择的 processor,并复用稳定的 Request 构建流程。

因此,这个例子真正值得记住的不是四个模式名称,而是四个判断:

接口不兼容,需要转换
同一目标存在多种实现
选择逻辑需要集中
公共流程已经稳定

设计模式应当从这些信号中长出来。它不是编码前必须完成的仪式,而是复杂度和变化方向变得明确后,对代码边界做出的回应。

Back to Blog

Related Posts

View All Posts »