设计模式实战解读(十一):外观模式——给复杂系统套一层壳

🔔 本文 6000+ 字深度原创,含完整代码示例和生产级落地方案。创作不易,如果对你有帮助,请点赞 👍 收藏 ⭐ 关注 🔥 三连支持,你的认可是我持续输出的最大动力!
本文是「设计模式实战解读」系列第十一篇。系列文章统一按照 定义 → 痛点场景 → 模式结构 → 核心实现 → 真实应用 → 常见变种 → 优缺点 → 避坑指南 → FAQ 的结构展开,每篇聚焦一个模式讲透。


一句话定义

外观模式(Facade):为子系统中的一组接口提供一个统一的高层接口,使子系统更容易使用。调用方不需要知道子系统内部有多少个组件、怎么协作,只需要跟一个"前台"打交道。

归属:结构型模式。


一、没有外观时的痛点

假设你在做一个 iPaaS 集成平台,要对接钉钉的审批流程。一个"发起审批"的动作,内部要做多少事?

java 复制代码
// 反面教材:调用方直接对接子系统
public class ApprovalController {

    @Autowired private DingTalkAuthService authService;
    @Autowired private DingTalkTokenService tokenService;
    @Autowired private DingTalkUserMappingService userMapping;
    @Autowired private DingTalkTemplateService templateService;
    @Autowired private DingTalkApprovalApiService approvalApi;
    @Autowired private ApprovalLogService logService;
    @Autowired private ApprovalNotifyService notifyService;

    public void startApproval(ApprovalRequest request) {
        // ① 获取访问令牌(可能需要刷新)
        String accessToken = tokenService.getAccessToken();

        // ② 校验用户权限
        authService.checkPermission(request.getUserId(), "approval:create");

        // ③ 把平台用户ID映射成钉钉的userId
        String dingUserId = userMapping.toDingUserId(request.getUserId());

        // ④ 查找审批模板
        String templateCode = templateService.findTemplate(request.getBizType());

        // ⑤ 把业务表单数据转成钉钉要求的JSON格式
        String formData = templateService.convertFormData(templateCode, request.getFields());

        // ⑥ 调用钉钉API发起审批
        String instanceId = approvalApi.createInstance(
            accessToken, templateCode, dingUserId, formData);

        // ⑦ 记录审批日志
        logService.logCreated(request.getUserId(), instanceId, request.getBizType());

        // ⑧ 通知相关人
        notifyService.notifyApprovers(instanceId, request.getApprovers());
    }
}

问题一目了然:

  1. 调用方心智负担极重------一个"发起审批"要调 8 个服务、8 步操作,调用方得把整个流程背下来
  2. 调用顺序容易出错------先拿 Token 还是先校验权限?搞反了就报错
  3. 强耦合------Controller 依赖了 7 个 Service,任何一个接口变动,Controller 都要改
  4. 难以复用------Webhook 触发审批、定时任务触发审批,都要重复写一遍这 8 步
  5. 错误处理散落------Token 过期了在哪一步重试?钉钉 API 限流了在哪一步降级?

核心诉求:给调用方一个简洁的接口------"发起审批",内部 8 步操作封装起来,调用方不需要也不应该知道这些细节。

这个"简洁的接口",就是外观(Facade)。


二、模式结构

复制代码
                    ┌─────────────────────────────────────┐
                    │         Facade(外观)               │
                    │  + startApproval(request): Result    │ ← 统一入口
                    │  + cancelApproval(id): Result        │
                    │  + queryApproval(id): Result         │
                    └───────────┬─────────────────────────┘
                                │ 内部编排
        ┌───────────────────────┼───────────────────────────┐
        │                       │                           │
        ↓                       ↓                           ↓
┌──────────────┐  ┌──────────────────┐  ┌────────────────────────┐
│  TokenService │  │ TemplateService  │  │ ApprovalApiService     │
│  AuthService  │  │ UserMapping      │  │ LogService             │
└──────────────┘  └──────────────────┘  │ NotifyService          │
                                        └────────────────────────┘

三个角色:

  • Facade(外观):统一对外的高层接口,内部编排子系统组件的调用顺序和错误处理
  • SubSystem(子系统):各个功能组件,各自只关注自己的职责
  • Client(调用方):只跟 Facade 打交道,不直接依赖子系统

调用方看到的:一个接口,三步搞定。子系统实际做的:八步编排,调用方完全无感。


三、核心实现

3.1 基础版:封装钉钉审批子系统

java 复制代码
/**
 * 审批外观------把 8 个子系统操作封装成一个简洁的接口
 */
@Service
public class ApprovalFacade {

    @Autowired private DingTalkTokenService tokenService;
    @Autowired private DingTalkAuthService authService;
    @Autowired private DingTalkUserMappingService userMapping;
    @Autowired private DingTalkTemplateService templateService;
    @Autowired private DingTalkApprovalApiService approvalApi;
    @Autowired private ApprovalLogService logService;
    @Autowired private ApprovalNotifyService notifyService;

    /**
     * 发起审批------调用方只需要传一个请求对象
     */
    public ApprovalResult startApproval(ApprovalRequest request) {
        // 1. 前置校验
        authService.checkPermission(request.getUserId(), "approval:create");
        String accessToken = tokenService.getAccessToken();
        String dingUserId = userMapping.toDingUserId(request.getUserId());

        // 2. 模板转换
        String templateCode = templateService.findTemplate(request.getBizType());
        String formData = templateService.convertFormData(templateCode, request.getFields());

        // 3. 调用钉钉API
        String instanceId = approvalApi.createInstance(
            accessToken, templateCode, dingUserId, formData);

        // 4. 后置处理
        logService.logCreated(request.getUserId(), instanceId, request.getBizType());
        notifyService.notifyApprovers(instanceId, request.getApprovers());

        return ApprovalResult.success(instanceId);
    }

    /**
     * 撤销审批
     */
    public void cancelApproval(String instanceId, String userId) {
        String accessToken = tokenService.getAccessToken();
        approvalApi.terminateInstance(accessToken, instanceId, userId);
        logService.logCancelled(userId, instanceId);
    }

    /**
     * 查询审批状态
     */
    public ApprovalStatus queryApproval(String instanceId) {
        String accessToken = tokenService.getAccessToken();
        return approvalApi.getInstanceStatus(accessToken, instanceId);
    }
}

调用方瞬间清爽了:

java 复制代码
// 修复后:Controller 只跟 Facade 打交道
@RestController
public class ApprovalController {

    @Autowired private ApprovalFacade approvalFacade;

    @PostMapping("/approval/start")
    public ApiResponse start(@RequestBody ApprovalRequest request) {
        ApprovalResult result = approvalFacade.startApproval(request);
        return ApiResponse.ok(result);
    }

    @PostMapping("/approval/cancel")
    public ApiResponse cancel(@RequestParam String instanceId,
                              @RequestParam String userId) {
        approvalFacade.cancelApproval(instanceId, userId);
        return ApiResponse.ok();
    }
}

Controller 从依赖 7 个 Service 变成依赖 1 个 Facade,代码从 30 行变成 3 行。这就是外观模式的力量------把复杂度关进笼子里。

3.2 进阶版:加上错误处理和降级

基础版有一个问题:如果中间某一步失败了怎么办?比如 Token 过期、钉钉限流、通知服务挂了------这些逻辑也应该在 Facade 里统一处理:

java 复制代码
@Service
public class ApprovalFacade {

    // ... 依赖注入同上

    public ApprovalResult startApproval(ApprovalRequest request) {
        try {
            // 1. 前置校验
            authService.checkPermission(request.getUserId(), "approval:create");

            // 2. 获取令牌(内置自动刷新)
            String accessToken = tokenService.getAccessToken();

            // 3. 用户映射 + 模板转换
            String dingUserId = userMapping.toDingUserId(request.getUserId());
            String templateCode = templateService.findTemplate(request.getBizType());
            String formData = templateService.convertFormData(
                templateCode, request.getFields());

            // 4. 调用钉钉API(内置重试)
            String instanceId = approvalApi.createInstanceWithRetry(
                accessToken, templateCode, dingUserId, formData);

            // 5. 记录日志(异步,不阻塞主流程)
            logService.logCreatedAsync(
                request.getUserId(), instanceId, request.getBizType());

            // 6. 通知审批人(降级:通知失败不影响审批创建)
            try {
                notifyService.notifyApprovers(instanceId, request.getApprovers());
            } catch (Exception e) {
                log.warn("通知审批人失败,降级处理: instanceId={}", instanceId, e);
                // 异步补偿:丢到MQ,后续重试
                notifyCompensationService.enqueue(instanceId, request.getApprovers());
            }

            return ApprovalResult.success(instanceId);

        } catch (PermissionDeniedException e) {
            return ApprovalResult.fail("PERMISSION_DENIED", e.getMessage());
        } catch (DingTalkRateLimitException e) {
            // 钉钉限流:返回友好提示,让前端引导用户稍后重试
            return ApprovalResult.fail("RATE_LIMITED", "钉钉接口繁忙,请稍后重试");
        } catch (Exception e) {
            log.error("发起审批异常: userId={}", request.getUserId(), e);
            return ApprovalResult.fail("SYSTEM_ERROR", "系统异常,请联系管理员");
        }
    }
}

Facade 不只是"编排调用顺序",还承担了三件事

  1. 错误处理统一出口------调用方不需要处理十几种异常,Facade 统一翻译成业务错误码
  2. 非关键步骤降级------通知失败不影响审批创建,日志写入改异步
  3. 重试和补偿------Token 自动刷新、API 调用重试、通知丢 MQ 补偿

这些横切逻辑如果散在调用方,根本管不住。


四、真实应用:iPaaS 连接器架构中的外观模式

我们在 iPaaS 平台的连接器架构里大量使用了外观模式。一个连接器(比如"钉钉连接器")对外暴露的是统一的操作接口,但内部涉及认证、协议适配、数据转换、限流、日志等多个子系统。

4.1 连接器外观

java 复制代码
/**
 * 连接器执行外观------所有连接器的统一入口
 */
@Service
public class ConnectorExecutionFacade {

    @Autowired private ConnectorAuthService authService;
    @Autowired private ConnectorRateLimiter rateLimiter;
    @Autowired private ProtocolAdapterRegistry adapterRegistry;
    @Autowired private DataTransformerRegistry transformerRegistry;
    @Autowired private ConnectorHttpClient httpClient;
    @Autowired private ConnectorLogService logService;
    @Autowired private ConnectorMetricsService metricsService;

    /**
     * 执行连接器操作------调用方只需传"连接器ID + 操作名 + 参数"
     */
    public ConnectorResult execute(ConnectorRequest request) {
        long startTime = System.currentTimeMillis();
        String traceId = TraceContext.getTraceId();

        try {
            // 1. 鉴权
            authService.authenticate(request.getConnectorId(), request.getCredentials());

            // 2. 限流
            rateLimiter.acquire(request.getConnectorId(), request.getAction());

            // 3. 协议适配------不同应用的API格式不一样
            ProtocolAdapter adapter = adapterRegistry.getAdapter(
                request.getConnectorId());
            HttpRequest httpRequest = adapter.buildRequest(
                request.getAction(), request.getParams());

            // 4. 发送HTTP请求
            HttpResponse httpResponse = httpClient.execute(httpRequest,
                request.getTimeout());

            // 5. 数据转换------把外部数据格式转成平台统一格式
            DataTransformer transformer = transformerRegistry.getTransformer(
                request.getConnectorId());
            Map<String, Object> result = transformer.transform(
                request.getAction(), httpResponse.getBody());

            // 6. 记录日志 + 打点
            long elapsed = System.currentTimeMillis() - startTime;
            logService.logSuccess(traceId, request, result, elapsed);
            metricsService.recordSuccess(request.getConnectorId(), elapsed);

            return ConnectorResult.success(result);

        } catch (AuthException e) {
            metricsService.recordFailure(request.getConnectorId(), "AUTH_ERROR");
            return ConnectorResult.fail("AUTH_ERROR", e.getMessage());
        } catch (RateLimitException e) {
            metricsService.recordFailure(request.getConnectorId(), "RATE_LIMITED");
            return ConnectorResult.fail("RATE_LIMITED", "调用频率超限");
        } catch (HttpTimeoutException e) {
            metricsService.recordFailure(request.getConnectorId(), "TIMEOUT");
            return ConnectorResult.fail("TIMEOUT", "第三方接口超时");
        } catch (Exception e) {
            long elapsed = System.currentTimeMillis() - startTime;
            logService.logFailure(traceId, request, e, elapsed);
            metricsService.recordFailure(request.getConnectorId(), "UNKNOWN");
            return ConnectorResult.fail("SYSTEM_ERROR", "执行异常");
        }
    }
}

调用方(流程引擎)只需要:

java 复制代码
// 流程引擎执行节点时,完全不需要知道"钉钉"和"企微"有什么区别
ConnectorResult result = connectorFacade.execute(
    new ConnectorRequest("dingtalk", "create_approval", params, credentials));

这就是外观模式在集成平台中的核心价值 ------600+ 个连接器,每个连接器的 API 都不一样(REST、SOAP、GraphQL、私有协议),但通过 Facade,流程引擎看到的永远是统一的 execute() 接口。

4.2 分层视角

#mermaid-svg-NlOUIpmgtl3rX5fR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NlOUIpmgtl3rX5fR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NlOUIpmgtl3rX5fR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NlOUIpmgtl3rX5fR .error-icon{fill:#552222;}#mermaid-svg-NlOUIpmgtl3rX5fR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NlOUIpmgtl3rX5fR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NlOUIpmgtl3rX5fR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NlOUIpmgtl3rX5fR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NlOUIpmgtl3rX5fR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NlOUIpmgtl3rX5fR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NlOUIpmgtl3rX5fR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NlOUIpmgtl3rX5fR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NlOUIpmgtl3rX5fR .marker.cross{stroke:#333333;}#mermaid-svg-NlOUIpmgtl3rX5fR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NlOUIpmgtl3rX5fR p{margin:0;}#mermaid-svg-NlOUIpmgtl3rX5fR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NlOUIpmgtl3rX5fR .cluster-label text{fill:#333;}#mermaid-svg-NlOUIpmgtl3rX5fR .cluster-label span{color:#333;}#mermaid-svg-NlOUIpmgtl3rX5fR .cluster-label span p{background-color:transparent;}#mermaid-svg-NlOUIpmgtl3rX5fR .label text,#mermaid-svg-NlOUIpmgtl3rX5fR span{fill:#333;color:#333;}#mermaid-svg-NlOUIpmgtl3rX5fR .node rect,#mermaid-svg-NlOUIpmgtl3rX5fR .node circle,#mermaid-svg-NlOUIpmgtl3rX5fR .node ellipse,#mermaid-svg-NlOUIpmgtl3rX5fR .node polygon,#mermaid-svg-NlOUIpmgtl3rX5fR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NlOUIpmgtl3rX5fR .rough-node .label text,#mermaid-svg-NlOUIpmgtl3rX5fR .node .label text,#mermaid-svg-NlOUIpmgtl3rX5fR .image-shape .label,#mermaid-svg-NlOUIpmgtl3rX5fR .icon-shape .label{text-anchor:middle;}#mermaid-svg-NlOUIpmgtl3rX5fR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NlOUIpmgtl3rX5fR .rough-node .label,#mermaid-svg-NlOUIpmgtl3rX5fR .node .label,#mermaid-svg-NlOUIpmgtl3rX5fR .image-shape .label,#mermaid-svg-NlOUIpmgtl3rX5fR .icon-shape .label{text-align:center;}#mermaid-svg-NlOUIpmgtl3rX5fR .node.clickable{cursor:pointer;}#mermaid-svg-NlOUIpmgtl3rX5fR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NlOUIpmgtl3rX5fR .arrowheadPath{fill:#333333;}#mermaid-svg-NlOUIpmgtl3rX5fR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NlOUIpmgtl3rX5fR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NlOUIpmgtl3rX5fR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NlOUIpmgtl3rX5fR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NlOUIpmgtl3rX5fR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NlOUIpmgtl3rX5fR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NlOUIpmgtl3rX5fR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NlOUIpmgtl3rX5fR .cluster text{fill:#333;}#mermaid-svg-NlOUIpmgtl3rX5fR .cluster span{color:#333;}#mermaid-svg-NlOUIpmgtl3rX5fR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NlOUIpmgtl3rX5fR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NlOUIpmgtl3rX5fR rect.text{fill:none;stroke-width:0;}#mermaid-svg-NlOUIpmgtl3rX5fR .icon-shape,#mermaid-svg-NlOUIpmgtl3rX5fR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NlOUIpmgtl3rX5fR .icon-shape p,#mermaid-svg-NlOUIpmgtl3rX5fR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NlOUIpmgtl3rX5fR .icon-shape .label rect,#mermaid-svg-NlOUIpmgtl3rX5fR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NlOUIpmgtl3rX5fR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NlOUIpmgtl3rX5fR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NlOUIpmgtl3rX5fR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 外部系统
子系统
外观层
调用方
流程引擎
Webhook触发器
定时任务
ConnectorExecutionFacade

统一 execute 接口
认证服务
限流服务
协议适配
数据转换
HTTP客户端
日志服务
监控打点
钉钉
企微
SAP
自定义应用

调用方不知道子系统有几个、协议怎么适配、数据怎么转换------它只知道"调 Facade,拿结果"。如果明天要加一个新的横切关注点(比如审计日志),只需要改 Facade,所有调用方零改动。


五、常见变种

5.1 多层外观

当子系统本身也很复杂时,可以分层嵌套 Facade:

复制代码
总 Facade(平台级)
  ├── 连接器 Facade(连接器管理)
  ├── 流程 Facade(流程编排与执行)
  └── 日志 Facade(日志采集与检索)

我们的 iPaaS 就是这么做的。平台对外提供一个 PlatformFacade,内部按业务域拆成若干子 Facade,每个子 Facade 再编排各自的子系统。

5.2 静态外观(工具类形式)

当 Facade 没有状态时,可以用静态方法:

java 复制代码
public class JsonUtils {
    private static final ObjectMapper mapper = new ObjectMapper();

    // Facade:把 Jackson 的复杂 API 封装成简洁的静态方法
    public static String toJson(Object obj) { /* ... */ }
    public static <T> T fromJson(String json, Class<T> clazz) { /* ... */ }
    public static JsonNode parse(String json) { /* ... */ }
}

JsonUtils 就是一个外观------你不需要知道 Jackson 的 ObjectMapperJsonNodeTypeReference 这些底层 API,只需要 toJson() / fromJson() 就够了。

5.3 门面 + 策略组合

Facade 内部可以根据不同的连接器类型,路由到不同的策略实现:

java 复制代码
public ConnectorResult execute(ConnectorRequest request) {
    // 根据连接器类型选择执行策略
    ConnectorStrategy strategy = strategyRegistry.getStrategy(
        request.getConnectorType());
    // 策略内部做具体的协议适配和数据转换
    return strategy.execute(request, commonContext);
}

Facade 负责"公共流程"(鉴权、限流、日志),策略负责"差异化流程"(协议适配、数据转换)。两者组合使用,既统一又灵活。


六、外观模式 vs 其他模式

对比维度 外观模式(Facade) 适配器模式(Adapter) 中介者模式(Mediator)
目的 简化接口,隐藏复杂度 转换接口,解决不兼容 解耦多对多通信
方向 单向:调用方 → Facade → 子系统 双向适配 多向协调
子系统是否知道 Facade 不知道 不知道 知道中介者的存在
典型场景 复杂 SDK 封装、第三方API对接 新旧接口兼容、三方SDK适配 聊天室、多方协作流程

容易混淆的点

  • Facade vs Adapter:两者都是"中间层",但 Facade 是"简化",Adapter 是"转换"。如果外部系统的 API 格式和你需要的不一样,用 Adapter;如果子系统太复杂、步骤太多,用 Facade
  • Facade vs Mediator:Facade 是单向的(调用方不知道子系统),Mediator 是双向的(组件之间通过中介者通信)。流程引擎编排多个节点是 Mediator,封装钉钉 API 是 Facade

七、优缺点

优点

  1. 降低调用方的使用成本------8 步操作变成 1 个方法调用
  2. 解耦调用方和子系统------子系统内部重构不影响调用方
  3. 统一错误处理------异常翻译、降级、重试都在 Facade 里集中管理
  4. 便于复用------多个调用方(Controller、Webhook、定时任务)共享同一套编排逻辑
  5. 渐进式演进------可以逐步把子系统的能力迁移到 Facade 中,不需要一次性重构

缺点

  1. Facade 容易变成"上帝对象"------所有编排逻辑都堆在 Facade 里,最终 Facade 也会膨胀
  2. 可能隐藏有用信息------过度封装后,调用方拿不到子系统的详细状态(比如"到底是认证失败还是限流")
  3. 不是所有场景都适合------如果调用方需要精细控制子系统的行为,Facade 反而成了障碍

八、避坑指南

坑 1:Facade 变成"大杂烩"

症状:Facade 类超过 500 行,方法内部嵌套 3-4 层 if-else,什么逻辑都往里塞。

修复 :Facade 只做编排和错误处理,不做业务逻辑。如果某段逻辑复杂到需要独立测试,就该抽到子系统里,Facade 只负责调用它。

java 复制代码
// 错误:Facade 里写了业务规则
public ApprovalResult startApproval(ApprovalRequest request) {
    if (request.getAmount() > 10000) { // ← 这是业务规则!
        request.setNeedDirectorApproval(true);
    }
    // ...
}

// 正确:业务规则在子系统的领域服务里
public ApprovalResult startApproval(ApprovalRequest request) {
    ApprovalFlow flow = approvalFlowService.resolveFlow(request); // 子系统决定审批流程
    // Facade 只做编排
}

坑 2:吞掉异常信息

症状:Facade 的 catch 块只返回"系统异常",把子系统的真实错误信息吞掉了,排查 Bug 时两眼一黑。

修复:错误码要能区分是哪个子系统出了问题,同时保留 traceId 方便追踪。

java 复制代码
// 错误:所有异常都返回"系统异常"
catch (Exception e) {
    return Result.fail("系统异常");
}

// 正确:区分错误来源 + 保留追踪信息
catch (AuthException e) {
    return Result.fail("AUTH_ERROR", e.getMessage(), traceId);
} catch (DingTalkApiException e) {
    return Result.fail("DINGTALK_API_ERROR", e.getErrorCode() + ": " + e.getMessage(), traceId);
}

坑 3:Facade 和子系统的边界不清

症状:有时候调 Facade,有时候直接调子系统------两种路径混用,导致某些横切逻辑(日志、鉴权)被绕过。

修复 :立规矩------调用方只能通过 Facade 访问子系统,禁止绕过 Facade 直接调子系统组件。可以通过包可见性(package-private)或模块边界来强制约束。

坑 4:过度封装

症状:一个本来只有两步操作的简单功能,也套了一层 Facade,反而增加了理解成本。

修复 :Facade 的价值在于简化复杂子系统的调用 。如果子系统本身就很简洁(1-2 个组件),没必要加 Facade。判断标准:调用方需要依赖 3 个以上子系统组件来完成一个操作时,才需要 Facade


九、常见问题(FAQ)

Q:外观模式和"三层架构"的 Service 层有什么区别?

A:Service 层是架构分层的一部分,承担业务逻辑;Facade 是设计模式,目的是简化子系统的调用。很多时候 Service 层的方法本身就起到了 Facade 的作用------它编排多个 DAO/中间件,对外提供简洁接口。两者的区别在于:Service 有业务规则,Facade 只做编排和封装。

Q:Facade 应该暴露子系统的异常还是统一翻译?

A:推荐翻译 。把子系统的技术异常翻译成业务异常码(AUTH_ERROR / RATE_LIMITED / TIMEOUT),调用方根据业务码做不同处理。同时保留原始异常的 traceId,方便排查。不要直接把 NullPointerException 抛给 Controller。

Q:一个系统可以有多个 Facade 吗?

A:可以,而且推荐。按业务域拆分 Facade,比如审批 Facade、通讯录 Facade、日程 Facade。不要搞一个"万能 Facade",那和"上帝对象"没区别。

Q:Facade 里可以有业务逻辑吗?

A:尽量不写。Facade 的职责是编排调用顺序 + 统一错误处理 + 降级策略。如果发现 Facade 里出现了 if-else 业务判断,说明这段逻辑应该下沉到子系统的领域服务里。

Q:Spring 的 @Service 类算不算 Facade?

A:看你怎么用。如果一个 @Service 只是透传调用(调一个 DAO 返回),那它不是 Facade。如果它编排了多个子系统组件(鉴权 + 缓存 + API调用 + 日志),那它实质上就是 Facade。模式不在于类名,而在于职责。

Q:Facade 和 BFF(Backend For Frontend)有什么关系?

A:BFF 本质上是 Facade 在架构层面的应用。BFF 为不同的前端(Web/App/小程序)提供不同的聚合接口,内部编排多个微服务。可以把 BFF 理解为"面向特定前端的 Facade"。


十、小结

外观模式是 23 种设计模式中最"朴实"的一个------没有花哨的继承、没有巧妙的委托,就是把复杂的东西包起来,给调用方一个简洁的接口。

但"朴实"不代表"简单"。一个好的 Facade 需要做好三件事:

  1. 编排------把多步操作串成一条流水线,调用方一个方法搞定
  2. 隔离------子系统的变化不影响调用方,调用方的需求变化不影响子系统
  3. 兜底------统一错误处理、降级、重试,不让异常泄漏到调用方

600+ 个连接器、几十种协议、无数的第三方 API------如果每个都让调用方直接对接,系统早就乱成一锅粥了。外观模式就是那个"前台",把混乱挡在门后,把简洁留给用户。


预告 :下一篇我们聊聊状态模式------当对象的行为随内部状态变化时,如何用"状态对象"替代满屏的 if-else。


标签:#设计模式 #外观模式 #Facade #结构型模式 #架构设计 #系统集成 #iPaaS #连接器 #代码质量 #解耦 #设计模式实战 #技术分享 #Java #研发效能

相关推荐
zmzb0103几秒前
Python课后习题训练记录Day129
开发语言·python
石一峰69920 分钟前
C 语言函数设计模式实战经验
c语言·开发语言·设计模式
秋923 分钟前
Python工程师面试常问提问和回答(AI工程化方向 · 2026版)
人工智能·python·面试
炎武丶航26 分钟前
LeNet-5深度学习详解:从手写数字识别到代码实战
人工智能·python·深度学习·机器学习·ai·cnn·lenet
sitellla26 分钟前
Pydub:用 Python 处理音频,不写废话
开发语言·python·其他·音视频
TechWayfarer36 分钟前
云服务器地域怎么选:用离线IP数据库识别用户来源并优化部署
服务器·数据库·python·tcp/ip·数据分析
梦想不只是梦与想39 分钟前
Python 中的进程(Process)
python·进程·进程间通
郑洁文40 分钟前
基于Python的恶意流量监测系统的设计与实现
开发语言·python
星辰徐哥40 分钟前
Python AI基础:Matplotlib与Seaborn数据可视化
人工智能·python·matplotlib
AI玫瑰助手42 分钟前
Python流程控制:for循环与range函数的搭配使用
开发语言·python·信息可视化