设计模式预计阅读 16 分钟6147 字1811 次浏览

设计模式修炼之路 ㉗ 业务代表模式——替表示层挡住远程的脏活

替表示层挡住远程脏活、查找+重试+异常翻译一肩挑、重试风暴、外观/代理/业务代表三兄弟对账、Feign 转世

目录

J2EE 模式第二站。

㉖ 的 Controller 直接持有 Catalog、直接调用,丝滑顺畅——因为它们在同一个进程里,一行代码就是一次方法调用的全部成本。可 EJB 时代的真实画风是:业务层住在另一台服务器上。表示层每次调用业务,都要先 JNDI 查找服务、再建网络连接、调完还得翻译远程异常、失败了还得琢磨要不要重试……Controller 迟早被这些"远程破事"淹没,沦为一个兼职网管。

在表示层和业务层之间修一条缓冲带:查找、重试、异常翻译这些与业务无关的机械细节,全部由它一个人消化——业务代表模式,J2EE 八式里的清道夫。

一、业务代表模式是什么

J2EE 蓝皮书里的定义大意:业务代表(Business Delegate)是表示层的一个对象,它替表示层去查找、访问业务服务,并处理服务定位、远程调用、异常翻译等底层细节,让表示层只跟业务语义打交道。

三个角色:

角色 干什么 本文对应
BusinessDelegate(业务代表) 表示层的业务代理:吸收查找/重试/异常翻译 OrderDelegate
BusinessLookup(业务查找) 负责定位业务服务(JNDI 的封装) ServiceLookup
BusinessService(业务服务) 业务层的接口,可能本地可能远程 OrderService / RemoteOrderService

一句话理解分工:表示层说业务话("下一单"),业务代表说技术话("查服务、建连接、抖了就重试、远程异常翻成本地异常"),业务层只管业务。

注意它不是 GoF 二十三式之一,是 Sun 的 J2EE 模式目录成员——那个年代"业务逻辑住在远程容器里"是默认设定,这个模式就是为远程边界而生的。

二、优点与缺点

优点:

  1. 表示层干净:Controller 里只剩业务调用,没有一行 JNDI、没有一坨重试循环——回看 ㉖,MVC 的职责分离才真正完整。
  2. 机械细节集中一处:重试几次、异常怎么翻译,改一处全体生效——反面教材里这些代码散在每个 Controller 方法里,改一次全项目搜索。
  3. 异常翻译:RemoteException、ConnectException 这些技术异常被翻译成业务异常,表示层不用 import 一堆远程包——远程性被封装掉了。
  4. 服务解耦:业务层从远程 EJB 迁回本地 Bean、或换成 HTTP 服务,只改业务代表内部,表示层无感。

缺点:

  1. 多一层皮:每个业务服务通常配一个代表类,接口容易沦为业务服务的镜像——为分层而分层。
  2. 单点偏差:重试、超时策略集中在代表身上,配错了影响所有入口(比如重试放大了下游压力——重试风暴)。
  3. 滞后性陷阱:代表封装的是"当时的业务接口",业务接口演化时代表要跟着改,慢一步就是两层不同步。

三、什么时候用

判断标准:表示层与业务层之间存在"脏边界"——服务查找、远程协议、重试容错、异常翻译,任何一样存在,就值得修缓冲带。

  • 远程业务层:EJB、RMI、早期的 WebService——本模式出生场景
  • 微服务客户端:调下游服务的所有机械逻辑(负载均衡、熔断、重试)想从 Controller 里赶走
  • 服务来源会变:今天本地 Bean、明天远程服务、后天改 HTTP——代表层挡住迁移波

反面对照:同进程调用——直接调,加一层纯属自虐;调用极简单且稳定——一个工具方法搞定,不必立类;已有框架代劳——用了 Feign/Dubbo 这类声明式客户端(第五节的当代转世),再手写业务代表就是双层保险过头。

四、实现方式:远程下单

4.1 反面教材:表示层直面远程服务

先造一个"远程服务"——模拟网络抖动(前 N 次调用失败):

public class RemoteOrderService implements OrderService {

    private int pendingFailures;

    public RemoteOrderService(int pendingFailures) {
        this.pendingFailures = pendingFailures;
    }

    @Override
    public String placeOrder(Order order) {
        System.out.println("      [远程调用] placeOrder(" + order.getId() + ") ... 网络往返中");
        if (pendingFailures-- > 0) {
            throw new IllegalStateException("网络抖动:连接超时(RemoteException 一族)");
        }
        return "订单 " + order.getId() + " 已受理(" + order.getProductName()
                + " x" + order.getQuantity() + ")";
    }
    // queryStock 同款,略
}

然后看 Controller 直面它时的惨状:

public class BadController {

    public String submitOrder(Order order) {
        System.out.println("    [JNDI 查找] OrderService ...(每个方法都要查一遍)");
        OrderService service = new RemoteOrderService(1);   // 模拟查找结果
        for (int attempt = 1; attempt <= 3; attempt++) {
            try {
                return "下单成功:" + service.placeOrder(order);
            } catch (IllegalStateException e) {
                System.out.println("    [Controller] 捕获远程异常 → 人肉翻译成业务话术(第 " + attempt + " 次失败)");
            }
        }
        return "系统繁忙,请稍后再试";
    }

    public String checkStock(String productId) {
        System.out.println("    [JNDI 查找] OrderService ...(同款查找,再抄一遍)");
        OrderService service = new RemoteOrderService(0);
        try {
            return "库存:" + service.queryStock(productId);
        } catch (IllegalStateException e) {
            return "系统繁忙,请稍后再试";
        }
    }
}

运行输出:

========== 反面教材:表示层直面远程服务 ==========
[表示层] controller.submitOrder(订单 #1001)
    [JNDI 查找] OrderService ...(每个方法都要查一遍)
      [远程调用] placeOrder(1001) ... 网络往返中
    [Controller] 捕获远程异常 → 人肉翻译成业务话术(第 1 次失败)
      [远程调用] placeOrder(1001) ... 网络往返中
  结果:下单成功:订单 1001 已受理(机械键盘 x1)
[表示层] controller.checkStock("P101")
    [JNDI 查找] OrderService ...(同款查找,再抄一遍)
      [远程调用] queryStock(P101) ... 网络往返中
  结果:库存:42
  —— 查找、重试、异常翻译,每个方法抄一遍,Controller 快成网管了 ——

功能没问题,问题在重复:查找、调用、异常翻译这套流程每个方法抄一遍;十个 Controller 方法就是十份拷贝;哪天重试策略从 3 次改成 5 次,全项目正则搜索。

4.2 正解:业务代表 + 业务查找

业务查找(JNDI 的封装,查到就缓存——这个缓存下一步会长大成 ㉜ 服务定位器模式):

public class ServiceLookup {

    private OrderService orderService;

    public OrderService getOrderService() {
        if (orderService == null) {
            System.out.println("      [JNDI 查找] OrderService ...(又慢又容易出错的一步)");
            orderService = new RemoteOrderService(2);
        }
        return orderService;
    }
}

业务代表:查找、重试、异常翻译一肩挑:

public class OrderDelegate {

    private final ServiceLookup lookup = new ServiceLookup();

    /** 表示层只需要这一行 */
    public String placeOrder(Order order) {
        OrderService service = lookup.getOrderService();
        IllegalStateException last = null;
        for (int attempt = 1; attempt <= 3; attempt++) {
            try {
                return service.placeOrder(order);
            } catch (IllegalStateException e) {
                System.out.println("      [业务代表] 第 " + attempt + " 次尝试失败,自动重试 ...");
                last = e;
            }
        }
        // 技术异常在这里翻译成业务异常,表示层拿不到 RemoteException
        throw new IllegalStateException("远程服务连续失败,请联系管理员(根因:" + last.getMessage() + ")");
    }

    public int queryStock(String productId) {
        return lookup.getOrderService().queryStock(productId);
    }
}

表示层的画风,只剩业务话:

OrderDelegate delegate = new OrderDelegate();

System.out.println("  结果:" + delegate.placeOrder(new Order("2001", "无线鼠标", 2)));
System.out.println("  结果:库存 " + delegate.queryStock("P101"));

运行输出:

========== 正解:业务代表替表示层挡枪 ==========
[表示层] delegate.placeOrder(订单 #2001)  ← 表示层只写这一行
      [JNDI 查找] OrderService ...(又慢又容易出错的一步)
      [远程调用] placeOrder(2001) ... 网络往返中
      [业务代表] 第 1 次尝试失败,自动重试 ...
      [远程调用] placeOrder(2001) ... 网络往返中
      [业务代表] 第 2 次尝试失败,自动重试 ...
      [远程调用] placeOrder(2001) ... 网络往返中
  结果:订单 2001 已受理(无线鼠标 x2)
[表示层] delegate.queryStock("P101")  ← 服务引用已缓存,不再 JNDI
      [远程调用] queryStock(P101) ... 网络往返中
  结果:库存 42
  —— 表示层只见业务语义,远程细节全被代表消化 ——

三处对照读出含金量:网络抖了两次,表示层毫不知情(重试在代表内部消化);JNDI 只查了一次(ServiceLookup 缓存了服务引用);第二次调用跳过查找直达远程。反面教材里 Controller 的十份拷贝,在这里是一份。

4.3 三处细节

细节一:异常翻译是本模式的灵魂。 注意 placeOrder 失败到底时抛的还是 IllegalStateException,但消息已经变成业务话术("远程服务连续失败,请联系管理员")——真实项目里该抛自定义的 OrderServiceException,表示层 catch 它时不需要 import 任何远程包。判断一个业务代表合不合格,就看表示层代码里还有没有远程世界的痕迹。

细节二:重试要警惕"重试风暴"。 本文演示 3 次重试很温柔,但想想:下游服务过载时,上游 10 个入口每个重试 3 次——流量瞬间放大成 30 倍,下游直接压死。生产级的重试必须配熔断(连续失败就熔一段时间)和退避(每次等待翻倍)——这两个词留给微服务时代,但思想种子在这里。

细节三:代表接口不必镜像业务接口。 本文 OrderDelegate 的方法与 OrderService 一一对应,是最简单的形制;实战里代表可以按表示层的用例重新组织——比如 checkout(Cart) 一个方法背后调远程的下单 + 扣库存 + 发通知三个服务。代表的接口是给表示层用的,不是给业务层的——它是缓冲带,不是传声筒。

五、在框架里找它

  1. OpenFeign(Spring Cloud):写个接口加 @FeignClient 注解,方法上写路径,框架生成实现——负载均衡、重试、超时全在框架层消化。Controller 注入这个接口直接调,见不到一行 RestTemplate。声明式客户端就是业务代表思想的当代转世:接口你定义,脏活框架干。

  2. Dubbo 的集群容错:failover(失败自动切换重试)、failfast(快速失败)、failsafe(失败忽略)——Dubbo 在消费者侧把"重试"做成了可配置策略。本文手写的 for 循环,在生产框架里是 Cluster 层的一个策略类(⑮ 的味道)。

  3. Resilience4j / Hystrix:熔断、重试、舱壁、降级四件套——专门治理"远程调用不可靠"的框架。业务代表模式里代表身上的职责,被拆成了可组合的装饰器(⑩ 的味道)。

  4. EJB 时代原装:Session Facade + Business Delegate + Service Locator 三件套是当年 Sun 蓝皮书的标准答案——本文 ServiceLookup 里那行缓存,就是 Service Locator 模式的种子,㉜ 正式展开。

六、业务代表 vs 外观 vs 代理——中间人三兄弟对账

细心的读者已经发现:业务代表长得像 ⑪ 外观,也像 ⑬ 代理——都是"中间垫一层"。三个中间人摊开对账:

外观(⑪) 代理(⑬) 业务代表(㉗)
出发点 子系统调用太碎 对目标的访问需要管控 表示层与业务层之间边界太脏
接口形态 通常重新设计、更粗 与目标**一模一样**,透明替换 按表示层需要设计,可重组
典型行为 多个调用收口成一个 延迟加载、权限检查、缓存 服务查找、重试容错、异常翻译
出身 GoF 1994 GoF 1994 J2EE 蓝皮书
一句话 替你**少打电话** 替你**管控电话** 替你**接通电话并处理杂音**

三者可以叠加:业务代表内部用代理拿远程引用、用外观收口多个服务调用——中间人不是排他的,按"你替我挡的是什么"来选:挡复杂度选外观,挡访问选代理,挡远程的脏选业务代表。

写在最后

  • 业务代表 = 表示层与业务层之间的缓冲带:查找、重试、异常翻译一肩挑
  • 表示层只见业务语义——判断合格的标准是它代码里没有远程世界的痕迹
  • 重试虽好,当心重试风暴:生产级必须配熔断与退避
  • 代表接口按表示层用例设计,是缓冲带不是传声筒
  • 中间人三兄弟:外观挡复杂度、代理挡访问、业务代表挡脏边界——可叠加,按需选

㉗ 的订单实体只有三个字段,一次远程调用就传输完毕——这是实体世界的"好养型"。可真实的领域模型里,"客户"这种实体由地址、信用、联系方式、订单历史一堆小对象组成;远程场景下,每个小对象都可能是独立的网络往返。EJB 的答案:把一组依赖对象捆成一个粗粒度实体统一管理——组合实体模式,下一篇见。

花

花开空白

开发者 · 西安

相关文章

评论(0)

还没有评论,来抢沙发吧

发表评论