1. 接口设计原则:单一职责在接口中的落地
接口设计这件事,我做了十几年,踩过的坑比写过的代码还多。刚开始带团队那会儿,总觉得接口设计就是定义几个方法签名,后来被线上故障教育了几次才明白——接口的边界,决定了系统的边界。
这一章,咱们就聊聊接口设计最核心的一条原则:单一职责。说白了,就是一个接口只干一件事,并且把它干好。
1.1 什么是接口的单一职责
你想想看,一个类如果有多个职责,改其中一个就可能影响另一个。接口也一样。如果一个接口包含了多个不相关的行为,那实现它的类就会被迫承担多个责任。
举个例子。我曾经接手过一个老项目,里面有个 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);
}
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);
}
你看,查询和写操作分开,因为它们的变化频率不同。查询接口可能几个月不变,写操作却可能因为业务规则调整频繁改动。状态机单独抽出来,是因为它有自己的复杂度。
1.3 高内聚低耦合的设计思想
高内聚低耦合,这个词你肯定听过。但在接口设计里,它到底怎么落地?
高内聚:一个接口内部的方法,应该围绕同一个业务目标。比如 OrderQueryService 里的所有方法,都是「查询订单信息」这个目标的子操作。
低耦合:接口之间尽量减少依赖。一个接口的变更,不应该导致另一个接口也必须跟着改。
我画了一张图,帮你理解这个关系:
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 接口设计自检清单
每次写完接口,我都会对照这个清单检查一遍。分享给你:
- 这个接口的名字能准确描述它的职责吗? 如果名字里带「And」,大概率该拆了。
- 接口里的方法是否都服务于同一个业务目标? 如果有方法跟其他方法「不搭」,考虑分离。
- 实现类是否会被迫实现不需要的方法? 如果有,说明接口粒度太粗。
- 接口的变更频率是否一致? 如果一部分方法经常改,另一部分很少改,考虑拆分。
- 接口的使用者是谁? 站在调用方的角度设计接口,而不是站在实现方的角度。
好了,这一章的内容就到这里。接口单一职责这件事,说起来简单,做起来需要不断练习。记住一句话:好的接口设计,是让使用者觉得「理所当然」。