1. 接口设计原则:单一职责在接口中的落地

接口设计这件事,我做了十几年,踩过的坑比写过的代码还多。刚开始带团队那会儿,总觉得接口设计就是定义几个方法签名,后来被线上故障教育了几次才明白——接口的边界,决定了系统的边界。

这一章,咱们就聊聊接口设计最核心的一条原则:单一职责。说白了,就是一个接口只干一件事,并且把它干好。

1.1 什么是接口的单一职责

你想想看,一个类如果有多个职责,改其中一个就可能影响另一个。接口也一样。如果一个接口包含了多个不相关的行为,那实现它的类就会被迫承担多个责任。

核心定义: 接口的单一职责原则(Interface Segregation Principle, ISP)——客户端不应该被迫依赖它不使用的接口。

举个例子。我曾经接手过一个老项目,里面有个 UserService 接口:

// 反面案例:一个接口塞了太多职责
public interface UserService {
    // 用户管理
    User findById(Long id);
    void createUser(User user);
    void updateUser(User user);
    void deleteUser(Long id);

    // 权限校验
    boolean hasPermission(Long userId, String permission);
    List<String> getRoles(Long userId);

    // 通知发送
    void sendWelcomeEmail(Long userId);
    void sendPasswordResetSms(String phone);

    // 日志记录
    void logUserAction(Long userId, String action);
}

这个接口有什么问题?太多了。实现类必须实现所有方法,哪怕它只需要用户查询功能。我当时重构这个模块时,发现一个简单的 UserQueryImpl 类,因为实现了这个接口,被迫写了四个空方法——sendWelcomeEmail 直接抛 UnsupportedOperationException。

嗯,这就是典型的接口职责过重。正确的做法是拆开:

// 拆分后的接口,各司其职
public interface UserQueryService {
    User findById(Long id);
}

public interface UserCommandService {
    void createUser(User user);
    void updateUser(User user);
    void deleteUser(Long id);
}

public interface PermissionService {
    boolean hasPermission(Long userId, String permission);
    List<String> getRoles(Long userId);
}

public interface NotificationService {
    void sendWelcomeEmail(Long userId);
    void sendPasswordResetSms(String phone);
}

public interface AuditService {
    void logUserAction(Long userId, String action);
}
我的习惯: 写接口之前,先问自己三个问题——这个接口的使用者是谁?他需要哪些方法?他不需要哪些方法?如果有一个方法超过 80% 的实现类都用不上,那就该拆。

1.2 接口粒度的把控

拆得太细也不行。我见过有人把接口拆到每个方法一个接口,结果一个业务逻辑要注入五六个接口,代码反而更难维护了。

那粒度怎么把握? 我个人总结了一个经验:按变化频率和变化原因来划分。

粒度 特征 适用场景 风险
过粗 一个接口包含多个不相关职责 快速原型、小型项目 实现类臃肿,变更影响面大
适中 每个接口聚焦一个业务维度 中大型项目、微服务 需要一定的设计经验
过细 每个方法一个接口 极端解耦场景 类爆炸,组合成本高

举个例子。假设你在做一个订单系统。订单有查询、创建、修改、取消、退款这些操作。你会怎么拆?

我建议这样:

// 订单查询:读操作,变化少
public interface OrderQueryService {
    Order findById(Long orderId);
    Page<Order> findByUserId(Long userId, Pageable pageable);
}

// 订单写操作:写操作,变化频繁
public interface OrderCommandService {
    Order createOrder(CreateOrderRequest request);
    void cancelOrder(Long orderId, String reason);
    void refundOrder(Long orderId, RefundRequest request);
}

// 订单状态机:状态流转逻辑,独立变化
public interface OrderStateMachine {
    boolean canTransition(OrderStatus from, OrderStatus to);
    OrderStatus transition(Order order, OrderStatus targetStatus);
}

你看,查询和写操作分开,因为它们的变化频率不同。查询接口可能几个月不变,写操作却可能因为业务规则调整频繁改动。状态机单独抽出来,是因为它有自己的复杂度。

避坑指南: 我曾经在一个项目中把订单接口拆成了 12 个细粒度接口,结果每个业务场景都要注入 4-5 个接口,代码可读性极差。后来我意识到——接口粒度不是越细越好,而是刚好满足使用者的需求。一个业务场景对应一个接口,是最容易理解的粒度。

1.3 高内聚低耦合的设计思想

高内聚低耦合,这个词你肯定听过。但在接口设计里,它到底怎么落地?

高内聚:一个接口内部的方法,应该围绕同一个业务目标。比如 OrderQueryService 里的所有方法,都是「查询订单信息」这个目标的子操作。

低耦合:接口之间尽量减少依赖。一个接口的变更,不应该导致另一个接口也必须跟着改。

我画了一张图,帮你理解这个关系:

接口设计:高内聚低耦合示意 高内聚接口 OrderQueryService findById() findByUserId() findByOrderNo() 所有方法围绕「订单查询」一个目标 低耦合接口 OrderQueryService OrderCommandService OrderStateMachine OrderEventPublisher 接口之间无直接依赖,各自独立演化 松耦合 高内聚:接口内部方法紧密围绕同一职责 | 低耦合:接口之间尽量减少直接依赖

1.4 实战:从糟糕接口到整洁接口

光说不练假把式。咱们看一个真实的重构案例。

这是我在一个电商项目中遇到的。原来的 PaymentService 接口长这样:

// 重构前:一个接口处理所有支付相关逻辑
public interface PaymentService {
    // 支付处理
    PaymentResult pay(Long orderId, BigDecimal amount, String paymentMethod);
    PaymentResult refund(Long paymentId, BigDecimal amount);

    // 对账
    List<ReconciliationRecord> reconcile(Date date);
    void uploadReconciliationFile(File file);

    // 渠道管理
    void addPaymentChannel(PaymentChannel channel);
    void removePaymentChannel(String channelCode);

    // 通知
    void notifyPaymentSuccess(Long orderId);
    void notifyRefundSuccess(Long paymentId);
}

这个接口的问题很明显:支付、对账、渠道管理、通知,四个完全不相关的职责混在一起。实现类有 3000 多行,每次改一个功能都要重新部署整个服务。

重构之后:

// 重构后:按职责拆分为四个接口
public interface PaymentExecutionService {
    PaymentResult pay(Long orderId, BigDecimal amount, String paymentMethod);
    PaymentResult refund(Long paymentId, BigDecimal amount);
}

public interface ReconciliationService {
    List<ReconciliationRecord> reconcile(Date date);
    void uploadReconciliationFile(File file);
}

public interface PaymentChannelService {
    void addChannel(PaymentChannel channel);
    void removeChannel(String channelCode);
}

public interface PaymentNotificationService {
    void notifyPaymentSuccess(Long orderId);
    void notifyRefundSuccess(Long paymentId);
}
重构效果:
  • 每个接口的代码量从 3000+ 行降到 300-500 行
  • 变更影响范围缩小了 70%
  • 单元测试覆盖率从 30% 提升到 85%
  • 新成员接手时,理解成本大幅降低

1.5 接口设计自检清单

每次写完接口,我都会对照这个清单检查一遍。分享给你:

  1. 这个接口的名字能准确描述它的职责吗? 如果名字里带「And」,大概率该拆了。
  2. 接口里的方法是否都服务于同一个业务目标? 如果有方法跟其他方法「不搭」,考虑分离。
  3. 实现类是否会被迫实现不需要的方法? 如果有,说明接口粒度太粗。
  4. 接口的变更频率是否一致? 如果一部分方法经常改,另一部分很少改,考虑拆分。
  5. 接口的使用者是谁? 站在调用方的角度设计接口,而不是站在实现方的角度。
一个小技巧: 写接口之前,先写测试。测试会逼着你思考——这个接口到底该怎么用?如果测试代码写起来很别扭,那接口设计大概率有问题。这就是 TDD 的魅力。

好了,这一章的内容就到这里。接口单一职责这件事,说起来简单,做起来需要不断练习。记住一句话:好的接口设计,是让使用者觉得「理所当然」。