15.1 远程外观

在细粒度对象之上提供粗粒度远程接口,减少网络往返并隔离内部对象模型。

学习目标

  • 能解释远程外观如何把多次细粒度网络请求收束为一次用例级调用,并指出领域规则仍由谁拥有
  • 能修改一个 TypeScript 远程外观,使内部服务调用被批量协调、返回数据传输对象,并保留超时与幂等边界
  • 能回答:面对延迟、重复提交和部分失败时,哪些运行证据支持采用远程外观,什么时候应该拒绝它

为什么远程外观值得单独学习

订单详情页看起来只是一个页面,但它可能同时需要订单、库存、付款和配送信息。如果客户端逐个请求这些信息,页面会被每一次等待牵着走;其中一次超时,还会让调用方猜测哪些结果已经生效。

本章解决的是“跨网络的一次业务意图应该如何表达”的问题。没有一个稳定的入口,客户端会把服务端内部对象和查询顺序暴露出去;有了它,客户端只描述用例,服务端负责在边界内协调细节。

代价也很具体:多了一层接口、编排和版本责任。只有当一次调用确实代表一个稳定用例,并且能用测量证明减少等待与耦合时,增加这层边界才值得。

先建立直觉:把很多次等待收束成一次意图

把客户端想成取货窗口:它只需要说“我要完成订单 42 的结算”,不需要知道店员先找订单、再查库存、再询问支付。就是这扇窗口。

窗口不应该替店员制定业务规则。它需要一个稳定的 ,可以按用例顺序调用内部服务、组装结果并统一返回;但库存是否足够、付款是否允许等决定,仍应由相应的应用服务或领域对象拥有。

判断收益时要记录三个可观察量:次数、载荷大小和失败位置。如果把十次远程 getter 换成一个大接口,却没有测量响应时间和错误定位,不能称为完成了优化。

目录单元到教学证据

15.1 远程外观

本章精确对应 manifest 单元 poeaa24-pattern-32-remote-facade,范围限定为:在细粒度对象之上提供粗粒度远程接口,隔离内部对象模型并减少网络往返。案例固定为订单与结算跨进程调用,所有判断都回到“客户端意图、接口契约、内部协调、失败传播”四个证据点。

完成本单元的证据不是记住模式名称,而是能交出一段可测试的 TypeScript 接口、一张标出网络边界的图,以及一次正常/超时/重复提交的对照记录。若客户端仍然需要知道 PaymentService 的调用顺序,远程外观的边界就没有成立。

代码实践:把客户端意图写成一次调用

案例是订单详情与结算。客户端只调用 getOrderDetailsplaceOrder;远程外观在服务端内部协调订单、库存和支付服务,并返回稳定的

type OrderDetails = { id: string; totalCents: number; paid: boolean };
type PlaceOrder = { orderId: string; idempotencyKey: string };
type PaymentResult = { paid: boolean; reference?: string };
 
interface OrderFacade {
  getOrderDetails(orderId: string): Promise<OrderDetails>;
  placeOrder(input: PlaceOrder): Promise<PaymentResult>;
}
 
class RemoteOrderFacade implements OrderFacade {
  constructor(
    private readonly orders: OrderService,
    private readonly payments: PaymentService,
  ) {}
 
  async getOrderDetails(orderId: string): Promise<OrderDetails> {
    const [order, payment] = await Promise.all([
      this.orders.readSummary(orderId),
      this.payments.readStatus(orderId),
    ]);
    return { id: order.id, totalCents: order.totalCents, paid: payment.paid };
  }
 
  async placeOrder(input: PlaceOrder): Promise<PaymentResult> {
    return this.payments.capture(input.orderId, input.idempotencyKey);
  }
}

这个边界有三点可测试。第一,客户端只看到用例级方法,不需要知道内部服务的数量和顺序;第二,返回值是稳定的数据传输对象,不是能被远程调用的领域对象;第三,写操作携带幂等键,重试同一个请求时由支付服务判断是否已经处理过。

远程外观可以减少客户端发出的网络调用,但它不能消除服务端内部的时间和失败。应记录一次请求包含哪些内部阶段、每个阶段的超时和最终状态;不要把“客户端只发一次”误报成“系统只做一次工作”。

专属可视化实验:观察边界、协调和失败

猜一猜:把客户端从三次远程调用改成一次 getOrderDetails 后,哪一类责任会移动到远程外观,哪一类责任仍不能移动?先写下你的预测,再用图切换阶段。

专属远程边界图 · 客户端意图
Remote Facade:一次用例调用,内部协调多个服务当前阶段:客户端只发出一次 getOrderDetails 请求,不依赖内部服务顺序。客户端业务意图:读取订单详情getOrderDetails(id)网络往返:1 次网络边界1 次请求RemoteFacadereadSummary(orderId)readStatus(orderId)负责协调,不复制领域规则组装稳定的 OrderDetails DTO返回 DTO客户端结果id: 42totalCents: 59700paid: true / false只暴露稳定字段通过条件:一次用例调用、稳定 DTO、可定位的失败状态,以及由领域服务拥有的业务规则。粗粒度远程接口将客户端意图与内部细粒度对象隔离
远程外观把一次业务意图映射为用例级调用,在服务端协调细节并返回 DTO;故障时保留可验证的失败位置。

主图默认展示一次用例调用。可以逐步查看客户端请求、远程外观协调内部服务和返回 DTO 三个阶段;“注入部分失败”会让支付阶段超时,帮助你检查失败是否仍然能定位到内部阶段。点击“重置”后,阶段、故障状态和说明都回到初始值。

分步1 / 3

1. 客户端只表达用例

客户端把“读取订单详情”作为一次远程请求发给远程外观,不再逐个访问订单、库存和支付对象。

专属远程边界图 · 客户端意图
Remote Facade:一次用例调用,内部协调多个服务当前阶段:客户端只发出一次 getOrderDetails 请求,不依赖内部服务顺序。客户端业务意图:读取订单详情getOrderDetails(id)网络往返:1 次网络边界1 次请求RemoteFacadereadSummary(orderId)readStatus(orderId)负责协调,不复制领域规则组装稳定的 OrderDetails DTO返回 DTO客户端结果id: 42totalCents: 59700paid: true / false只暴露稳定字段通过条件:一次用例调用、稳定 DTO、可定位的失败状态,以及由领域服务拥有的业务规则。粗粒度远程接口将客户端意图与内部细粒度对象隔离
远程外观把一次业务意图映射为用例级调用,在服务端协调细节并返回 DTO;故障时保留可验证的失败位置。

失败、重试与版本边界

写操作不能只依赖网络层重试。应由客户端为一次业务意图生成,并由拥有副作用的服务保存处理结果;远程外观负责透传并保持这个契约,而不是在自己这一层猜测付款是否成功。

是分布式调用的常态。订单读取可以返回库存信息但缺少付款状态,结算也可能已经扣款却没有及时收到确认。接口需要区分“未执行”“执行中”“已成功但响应丢失”和“明确失败”,否则重试会制造重复副作用。

远程外观的版本只承诺稳定的用例契约。新增可选字段通常比暴露一个新的内部服务方法安全;把数据库实体原样序列化、让客户端依赖内部类名或把所有内部字段塞进一个“万能响应”,都会让内部重构变成跨网络破坏性变更。

评审问题采用远程外观的证据应拒绝或改用其他方案的信号
粒度一次调用对应一个稳定用例,内部细节可独立变化方法只是把每个 getter 原样搬到网络上
性能基线与改造后都记录往返、载荷和 P95 延迟只凭“请求次数变少”宣称一定更快
失败有超时、重复响应和部分失败的状态契约只能返回一个模糊的 500,无法定位阶段
边界返回 DTO,领域规则仍由服务端拥有客户端直接修改或缓存内部领域对象

常见误区

本章小结

  • 远程外观把一次客户端意图表达为粗粒度远程调用。
  • 外观协调内部服务,但不拥有库存、付款等领域规则。
  • DTO 隔离内部对象;幂等键约束写操作重试。
  • 用往返、载荷、延迟和失败位置验证是否真的获得收益。

练习与答案

练习

问题 1:修改 Demo 代码。 把示例中的 getOrderDetails 改成只暴露一次远程调用的批量查询,并为 placeOrder 增加“重复幂等键返回同一结果”的测试。你会把哪条规则放在远程外观,哪条规则放在支付服务?

问题 2:验证收益。 改造前客户端发出 3 次远程请求,改造后发出 1 次,但 P95 延迟从 180 ms 变成 240 ms。你会如何判断这次改造是否仍然成立?

问题 3:选择或拒绝。 一个客户端只需要读取一个很小的本地值,但团队想先建立远程外观以“保持架构统一”。你会要求什么证据后再决定?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

远程外观

放在网络边界上的粗粒度入口;客户端调用一次用例,入口在服务端协调内部细节。

粗粒度契约

一次调用携带一个完整业务意图,而不是把一个小对象的每个属性拆成多次网络请求。

往返

请求从客户端到服务端再回到客户端的来回次数;网络越远、次数越多,等待通常越明显。

数据传输对象

只装跨边界所需字段、可以序列化和版本化的数据载体,不把服务端对象行为暴露给客户端。

幂等键

标记一次业务意图的请求标识;同一个键重试时,拥有副作用的服务应返回同一处理结果而不重复执行。

部分失败

一次跨服务操作只有部分步骤完成或响应丢失的状态,需要查询、补偿或继续处理的明确规则。

前后导航

参考资料

资料与写作方式声明

本章以Patterns of Enterprise Application Architecture权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…