接口设计中的扩展与组合:一次Code Review引发的思考

2025-12-12 0 624

背景

最近在开发微服务项目时遇到了一个典型的技术决策场景:我们的商品服务通过 Dubbo 对外提供了一个商品列表查询的 RPC 接口,目前被网页端系统调用。现在 PC 端管理系统(新的调用方)也需要类似的商品列表功能。在 Code Review 过程中,关于是新建接口还是复用现有接口,组长和我产生了分歧。

这次争论让我意识到:同样是\”代码复用\”,不同人看到的场景也可能是不同的。

项目架构:

┌─────────────────┐
│ 订单服务        │ ────┐
│ order-service   │     │
└─────────────────┘     │
                        │
┌─────────────────┐     │      ┌──────────────────────────┐
│ PC管理系统      │ ────┼─────→│ 商品服务                 │
│ pc-admin        │     │      │ product-service          │
└─────────────────┘     │      │                          │
                        │      │ ProductFacade (Dubbo)    │
┌─────────────────┐     │      │  ├─ queryProductList()   │
│ 网页商城        │ ────┘      │  └─ ??? 新增方法 ???    │
│ web-mall        │            └──────────────────────────┘
└─────────────────┘

具体场景

  • 现有接口:商品服务对外提供的 RPC 接口(被网页商城调用,包含热度计算、推荐数据等额外查询)
  • 新需求:PC 端管理系统需要商品列表功能(只需要基本查询,用于后台管理)
  • 我的方案:在 Facade 接口中新增一个方法 queryProductListForPc()
  • 质疑:为什么不在现有方法 queryProductList() 加个 source 参数来区分,代码差不多为什么要重复?

两种设计思路的对比

方案 A: 在请求对象中新增可选字段,通过判空兼容

/**
 * 商品服务对外 Facade 接口(API 层)
 * 调用方:web-mall, pc-admin, order-service
 */
public interface ProductFacade {

    /**
     * 查询商品列表(保持方法签名不变,向后兼容)
     */
    Result<List<ProductDTO>> queryProductList(ProductQueryRequest request);
}
// 请求对象(在 API jar 中)
@Data
public class ProductQueryRequest implements Serializable {
    private Integer pageNum;
    private Integer pageSize;
    private String source;    // 新增字段:来源标识(可选,老调用方不传)
}
// product-service-impl 实现
@Service
@DubboService
public class ProductFacadeImpl implements ProductFacade {

    @Override
    public Result<List> queryProductList(ProductQueryRequest request) {
        List<Productproducts = productService.queryBasicList(
            request.getPageNum(),
            request.getPageSize()
        );

        // 判断 source 字段是否为空(向后兼容的关键)
        if (request.getSource() == null || \"web\".equals(request.getSource())) {
            // 老调用方不传 source,默认按 Web 端处理
            // 或者显式传 \"web\"
            enrichWebData(products);          // +50ms
            calculateWebMetrics(products);    // +80ms
            loadWebRecommendations(products); // +70ms
        } else if (\"pc\".equals(request.getSource())) {
            // PC端逻辑(或者不做额外处理)
        }

        return Result.success(ProductConverter.toDTO(products));
    }
}

优点

  • 只修改请求对象,不修改方法签名,向后兼容
  • 老调用方不传 source,代码不用改,也能正常运行
  • 只有一个方法,\”代码复用率高\”,后期可以统一维护,认知成本低(实际上依赖对 source 字段的维护,认知成本也不低)

可能存在的问题

  1. 隐式默认值问题,容易出错

    // 问题:source == null 默认是 Web 端,这是一个隐式假设
    if (request.getSource() == null || \"web\".equals(request.getSource())) {
        // 执行 Web 端逻辑
    }
    
    // 隐患:
    // 1. 订单服务调用时,不知道会走 Web 端逻辑,可能执行了不需要的查询
    // 2. 未来如果有第4个、第5个调用方,默认值假设可能不成立
    // 3. 新人维护代码时,不清楚 null 代表什么含义
    
  2. 职责不清晰,一个方法服务多个场景

    // 这个方法既要服务 Web 端,又要服务 PC 端,还可能要服务 App 端
    // 随着业务发展,if-else 会越来越多
    if (source == null || \"web\".equals(source)) {
        // Web端逻辑(50行)else if (\"pc\".equals(source)) {
        // PC端逻辑(80行)else if (\"app\".equals(source)) {
        // App端逻辑(60行)
    }
    // 这个方法会膨胀到200行+
    
  3. 各端逻辑耦合,改一处影响全局

    // 场景:Web 端要优化性能,调整了基础查询逻辑
    List products = productService.queryBasicList(pageNum, pageSize);
    
    // 问题:这个改动会影响 PC 端、App 端的所有调用
    // 需要回归测试所有端的所有场景
    

方案 B:我的方案 – 新增独立方法

/**
 * 商品服务对外 Facade 接口(API 层)
 */
public interface ProductFacade {

    /**
     * 查询商品列表(保持原有签名不变,向后兼容)
     */
    Result<List<ProductDTO>> queryProductList(ProductQueryRequest request);

    /**
     * 查询商品列表 - PC端(新增方法,专门给PC管理端使用)
     */
    Result<List<ProductDTO>> queryProductListForPc(ProductQueryRequest request);
}
// 请求对象保持不变
@Data
public class ProductQueryRequest implements Serializable {
    private Integer pageNum;
    private Integer pageSize;
    // 不添加 source 字段
}
// product-service-impl 实现
@Service
@DubboService
public class ProductFacadeImpl implements ProductFacade {

    /**
     * 查询商品列表(保持原有逻辑不变,服务 Web 端和老调用方)
     */
    @Override
    public Result<List> queryProductList(ProductQueryRequest request) {
        List<Productproducts = productService.queryBasicList(
            request.getPageNum(),
            request.getPageSize()
        );

        // Web端特有的数据增强
        enrichWebData(products);          // +50ms
        calculateWebMetrics(products);    // +80ms
        loadWebRecommendations(products); // +70ms

        return Result.success(ProductConverter.toDTO(products));
    }

    /**
     * PC端商品列表(新增,轻量级)
     */
    @Override
    public Result<List<ProductDTO>> queryProductListForPc(ProductQueryRequest request) {
        // 只做基础查询,不做额外增强
        List<Productproducts = productService.queryBasicList(
            request.getPageNum(),
            request.getPageSize()
        );
        return Result.success(ProductConverter.toDTO(products));
    }
}

优点

1. 完全向后兼容,老调用方零改动

// 订单服务、Web商城的调用代码完全不需要改
ProductQueryRequest request = new ProductQueryRequest();
request.setPageNum(1);
request.setPageSize(10);
Result<List> result = productFacade.queryProductList(request);

// PC管理系统调用新方法
ProductQueryRequest request = new ProductQueryRequest();
request.setPageNum(1);
request.setPageSize(10);
Result<List> result = productFacade.queryProductListForPc(request);

2. 独立升级,互不影响

  • 商品服务发布新版本 API jar 包(新增了 queryProductListForPc 方法)
  • PC 管理系统升级 jar 包,调用新方法
  • 订单服务、Web 商城不用动,继续用老方法 queryProductList
  • 各自按自己的节奏升级

3. 零风险上线

  • 新方法只服务新调用方(PC 管理系统)
  • 老方法保持不变,所有老调用方不受影响
  • 不存在\”遗漏某个调用方\”的问题

4. 职责清晰,独立演进

queryProductList()       // 服务 Web端和老调用方,未来加推荐、短视频
queryProductListForPc()  // 服务 PC端,未来加ERP、批量导入

// 两个方法各管各的,互不干扰

5. 性能优化,各取所需

// Web 端调用:执行完整逻辑,耗时 200ms
productFacade.queryProductList(request);  // 含推荐、热度等

// PC 端调用:只查基础数据,耗时 10ms
productFacade.queryProductListForPc(request);  // 轻量级

// 老调用方(订单服务):继续走原有逻辑
productFacade.queryProductList(request);  // 虽然有冗余查询,但向后兼容

6. 便于监控和问题排查

// 可以分别监控:
// - queryProductList 的调用量、耗时、错误率(Web端 + 老调用方)
// - queryProductListForPc 的调用量、耗时、错误率(PC端)

// 出问题时能快速定位是哪个端的调用

缺点

  • Facade 接口方法数量增加(但每个方法职责更清晰)
  • 存在一定程度的代码重复(但可以通过 Service 层复用解决)
  • 存在认知成本,换个开发看到好几个获取商品列表的方法会有点困惑

为什么我们的想法会不同?

讨论到这里,我突然意识到一个问题:我们实际讨论的是扩展和组合。

一个成功的组合案例:退款逻辑合并

在我们的项目中,曾经进行过一次重构,把两条独立的退款逻辑成功合并到了一起,进行统一维护。

重构前的状态

// PC 端退款逻辑(已稳定运行 1 年+)
public class PcRefundService {
    public RefundResult refundForPc(RefundRequest request) {
        // 1. PC 端特有的审核流程(多级审批)
        pcAuditService.audit(request);

        // 2. 调用第三方支付退款
        paymentGateway.refund(request);

        // 3. PC 端特有的凭证生成
        pcVoucherService.generateVoucher(request);

        // 4. 同步到 ERP 系统
        erpService.syncRefund(request);
    }
}

// Web 端退款逻辑(已稳定运行 8 个月)
public class WebRefundService {
    public RefundResult refundForWeb(RefundRequest request) {
        // 1. Web 端特有的风控检查
        riskControlService.check(request);

        // 2. 调用第三方支付退款(同样的支付网关)
        paymentGateway.refund(request);

        // 3. Web 端特有的积分返还
        pointService.returnPoints(request);

        // 4. 发送通知给用户
        notificationService.notify(request);
    }
}

重构后的状态

public class UnifiedRefundService {
    public RefundResult refund(RefundRequest request) {
        // 1. 根据来源执行不同的前置流程
        if (\"pc\".equals(request.getSource())) {
            pcAuditService.audit(request);
        } else if (\"web\".equals(request.getSource())) {
            riskControlService.check(request);
        }

        // 2. 通用的退款核心逻辑(提取出来)
        paymentGateway.refund(request);

        // 3. 根据来源执行不同的后置流程
        if (\"pc\".equals(request.getSource())) {
            pcVoucherService.generateVoucher(request);
            erpService.syncRefund(request);
        } else if (\"web\".equals(request.getSource())) {
            pointService.returnPoints(request);
            notificationService.notify(request);
        }
    }
}

这次合并为什么成功?

  • 两条退款逻辑都已经稳定运行很久,业务边界清晰
  • 核心流程(调用支付网关)确实可以复用
  • 合并后维护更方便,bug 修复只需改一处
  • 新增其他端的退款(如 App 端)也很容易扩展

成功的关键前提:

  1. 两条业务线都已经独立演进到稳定状态
  2. 业务边界已经清晰
  3. 确实发现了可复用的核心逻辑
  4. 这是对内的重构,不影响外部调用方

当前场景:商品列表查询

但这次商品列表查询是否满足组合条件呢?我认为不满足

关键差异对比

维度 退款合并(成功的组合) 商品列表(当前场景)
阶段 两条线都已经稳定运行很久 PC 端是全新需求,还没开始
业务理解 业务边界清晰,知道哪些可以复用 PC 端需求还不明确,可能会变
复用价值 确实有核心逻辑可以提取 只是都叫\”商品列表\”,实际逻辑差异大
方向 对内重构,优化内部结构 对外扩展,增加新能力

扩展 vs 组合的本质区别

概念定义

扩展(Extension)- 对外增加能力

  • 目的:给系统增加新的对外能力
  • 时机:新需求、新业务线
  • 原则:各自独立,给予足够的演进空间
  • 示例:新增 PC 端商品列表接口

组合(Composition)- 对内优化结构

  • 目的:重构内部实现,消除重复
  • 时机:多条业务线稳定后,发现可复用模式
  • 原则:提取共性,保持灵活性
  • 示例:合并 PC 端和 Web 端的退款逻辑

核心原则:先扩展,后组合

退款逻辑的重构本质上也是先进行了扩展,然后再进行组合

为什么退款会先进行扩展?

  1. 时间成本 – 本质上还是两个独立的业务,如果在开发之初就尝试组合复用,会极大增加时间成本
  2. 需求变化 – 新的业务免不了调整,这时业务边界不清晰,很容易造成影响
  3. 兼容成本 – 如果在需求不清晰的时候就考虑兼容,很容易处处受限

为什么商品列表会让人考虑直接组合?

  1. 业务逻辑简单 – 本质就是一个 CRUD,兼容成本看起来很低
  2. 需求变化小 – 感觉不太会有太多的变化

但实际上这两者是一样的,它们都是对外增加了新的能力,而不能因为业务简单就直接考虑进行组合。


总结

这次 Code Review 让我明白了几个道理:

1. 不要混淆\”对外扩展\”和\”对内组合\”

  • 对外扩展时:优先考虑独立性和灵活性
  • 对内组合时:可以考虑复用和统一

2. 不要过早组合

  • 退款合并是成功的,因为是\”先扩展,后组合\”
  • 如果在 PC 端商品列表刚立项时就组合,是\”过早组合\”

3. 分阶段演进

  1. 扩展期:新增独立接口,给予足够的演进空间
  2. 稳定期:各自独立发展,积累业务理解
  3. 组合期:发现可复用模式,进行内部重构

4. 当前场景的决策

  • 现在:采用方案 B,新增独立的 queryProductListForPc() 方法
  • 未来:等 PC 端商品列表也稳定运行半年后,再评估是否需要组合
  • 原则:先让子弹飞一会儿,不要急着优化

最重要的是:扩展总不会错,不会影响老逻辑,尤其是在小公司快速迭代的环境下。


关注「9号达人」公众号,获取更多干货

我是一名小厂程序员,专注分享真实的项目实战经验接地气的技术思考

关注公众号「9号达人」

不讲大而全的理论,只聊真实踩过的坑。

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 编程相关 接口设计中的扩展与组合:一次Code Review引发的思考 https://www.zuozi.net/36008.html

常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务