15.1 远程外观
在细粒度对象之上提供粗粒度远程接口,减少网络往返并隔离内部对象模型。
学习目标
- 能解释远程外观如何把多次细粒度网络请求收束为一次用例级调用,并指出领域规则仍由谁拥有
- 能修改一个 TypeScript 远程外观,使内部服务调用被批量协调、返回数据传输对象,并保留超时与幂等边界
- 能回答:面对延迟、重复提交和部分失败时,哪些运行证据支持采用远程外观,什么时候应该拒绝它
为什么远程外观值得单独学习
订单详情页看起来只是一个页面,但它可能同时需要订单、库存、付款和配送信息。如果客户端逐个请求这些信息,页面会被每一次等待牵着走;其中一次超时,还会让调用方猜测哪些结果已经生效。
本章解决的是“跨网络的一次业务意图应该如何表达”的问题。没有一个稳定的入口,客户端会把服务端内部对象和查询顺序暴露出去;有了它,客户端只描述用例,服务端负责在边界内协调细节。
代价也很具体:多了一层接口、编排和版本责任。只有当一次调用确实代表一个稳定用例,并且能用测量证明减少等待与耦合时,增加这层边界才值得。
先建立直觉:把很多次等待收束成一次意图
把客户端想成取货窗口:它只需要说“我要完成订单 42 的结算”,不需要知道店员先找订单、再查库存、再询问支付。↡位于远程边界上的粗粒度接口;客户端用一次用例调用它,由它在服务端协调多个细粒度对象。就是这扇窗口。
窗口不应该替店员制定业务规则。它需要一个稳定的 ↡一次调用表达完整业务意图、而不是把一个小对象的属性拆成多次网络请求的接口约定。,可以按用例顺序调用内部服务、组装结果并统一返回;但库存是否足够、付款是否允许等决定,仍应由相应的应用服务或领域对象拥有。
判断收益时要记录三个可观察量:↡一次请求从客户端发出到收到对应响应所经历的网络来回;网络调用次数增加通常会放大它。次数、载荷大小和失败位置。如果把十次远程 getter 换成一个大接口,却没有测量响应时间和错误定位,不能称为完成了优化。
目录单元到教学证据
15.1 远程外观
本章精确对应 manifest 单元 poeaa24-pattern-32-remote-facade,范围限定为:在细粒度对象之上提供粗粒度远程接口,隔离内部对象模型并减少网络往返。案例固定为订单与结算跨进程调用,所有判断都回到“客户端意图、接口契约、内部协调、失败传播”四个证据点。
完成本单元的证据不是记住模式名称,而是能交出一段可测试的 TypeScript 接口、一张标出网络边界的图,以及一次正常/超时/重复提交的对照记录。若客户端仍然需要知道 PaymentService 的调用顺序,远程外观的边界就没有成立。
代码实践:把客户端意图写成一次调用
案例是订单详情与结算。客户端只调用 getOrderDetails 或 placeOrder;远程外观在服务端内部协调订单、库存和支付服务,并返回稳定的 ↡只包含跨边界所需字段、可以序列化和版本化的数据载体;它不把服务端对象行为带到客户端。。
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 后,哪一类责任会移动到远程外观,哪一类责任仍不能移动?先写下你的预测,再用图切换阶段。
主图默认展示一次用例调用。可以逐步查看客户端请求、远程外观协调内部服务和返回 DTO 三个阶段;“注入部分失败”会让支付阶段超时,帮助你检查失败是否仍然能定位到内部阶段。点击“重置”后,阶段、故障状态和说明都回到初始值。
1. 客户端只表达用例
客户端把“读取订单详情”作为一次远程请求发给远程外观,不再逐个访问订单、库存和支付对象。
失败、重试与版本边界
写操作不能只依赖网络层重试。↡让同一个业务请求被处理一次或多次时,都不会产生额外非预期副作用的请求标识。应由客户端为一次业务意图生成,并由拥有副作用的服务保存处理结果;远程外观负责透传并保持这个契约,而不是在自己这一层猜测付款是否成功。
↡一次跨服务用例中,部分步骤已返回结果、另一些步骤失败或超时的状态;它需要明确的回退、补偿或查询策略。是分布式调用的常态。订单读取可以返回库存信息但缺少付款状态,结算也可能已经扣款却没有及时收到确认。接口需要区分“未执行”“执行中”“已成功但响应丢失”和“明确失败”,否则重试会制造重复副作用。
远程外观的版本只承诺稳定的用例契约。新增可选字段通常比暴露一个新的内部服务方法安全;把数据库实体原样序列化、让客户端依赖内部类名或把所有内部字段塞进一个“万能响应”,都会让内部重构变成跨网络破坏性变更。
| 评审问题 | 采用远程外观的证据 | 应拒绝或改用其他方案的信号 |
|---|---|---|
| 粒度 | 一次调用对应一个稳定用例,内部细节可独立变化 | 方法只是把每个 getter 原样搬到网络上 |
| 性能 | 基线与改造后都记录往返、载荷和 P95 延迟 | 只凭“请求次数变少”宣称一定更快 |
| 失败 | 有超时、重复响应和部分失败的状态契约 | 只能返回一个模糊的 500,无法定位阶段 |
| 边界 | 返回 DTO,领域规则仍由服务端拥有 | 客户端直接修改或缓存内部领域对象 |
常见误区
本章小结
- 远程外观把一次客户端意图表达为粗粒度远程调用。
- 外观协调内部服务,但不拥有库存、付款等领域规则。
- DTO 隔离内部对象;幂等键约束写操作重试。
- 用往返、载荷、延迟和失败位置验证是否真的获得收益。
练习与答案
练习
问题 1:修改 Demo 代码。 把示例中的 getOrderDetails 改成只暴露一次远程调用的批量查询,并为 placeOrder 增加“重复幂等键返回同一结果”的测试。你会把哪条规则放在远程外观,哪条规则放在支付服务?
问题 2:验证收益。 改造前客户端发出 3 次远程请求,改造后发出 1 次,但 P95 延迟从 180 ms 变成 240 ms。你会如何判断这次改造是否仍然成立?
问题 3:选择或拒绝。 一个客户端只需要读取一个很小的本地值,但团队想先建立远程外观以“保持架构统一”。你会要求什么证据后再决定?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 远程外观
放在网络边界上的粗粒度入口;客户端调用一次用例,入口在服务端协调内部细节。
- 粗粒度契约
一次调用携带一个完整业务意图,而不是把一个小对象的每个属性拆成多次网络请求。
- 往返
请求从客户端到服务端再回到客户端的来回次数;网络越远、次数越多,等待通常越明显。
- 数据传输对象
只装跨边界所需字段、可以序列化和版本化的数据载体,不把服务端对象行为暴露给客户端。
- 幂等键
标记一次业务意图的请求标识;同一个键重试时,拥有副作用的服务应返回同一处理结果而不重复执行。
- 部分失败
一次跨服务操作只有部分步骤完成或响应丢失的状态,需要查询、补偿或继续处理的明确规则。
前后导航
参考资料
- Martin Fowler:Remote Facade 模式摘要:核对模式的公开定义与远程边界。
- Martin Fowler:Patterns of Enterprise Application Architecture:核对图书主题、章节范围和模式目录关系。
- Pearson:Patterns of Enterprise Application Architecture:交叉核对出版信息与目录范围。