模块接口设计:接口定义规范、参数传递方式、返回值设计、异常处理策略
模块接口设计,说白了就是给模块画个「门面」。
你想想看,一个系统里几十上百个模块,大家怎么协作?靠的就是接口。接口设计得好,模块之间就像乐高积木,随便拼。设计得不好,那就是一团乱麻,改一个地方崩一片。
我个人习惯,在开始写任何模块代码之前,先把接口定死。接口不动,内部随便重构。这就像盖楼先画图纸,图纸定了,施工队才能进场。
接口定义规范:先定规矩,再干活
接口定义,核心就四个字:清晰、稳定。
我在项目中遇到过最头疼的事,就是接口命名混乱。有人用 getUserInfo,有人用 fetchUserData,还有人用 queryUser。同一个意思,三种写法。后期维护时,你根本不知道哪个是哪个。
我建议的命名规范:
- 动词 + 名词:
createOrder、deleteUser、updateConfig - 查询用 get/find:
getUserById、findOrdersByStatus - 操作用 do/process:
doPayment、processRefund - 避免缩写:别用
usr代替user,别用cfg代替config
还有一个容易被忽略的点:接口版本号。我习惯在接口名里带上版本,比如 getUserV1、getUserV2。这样升级接口时,老版本还能继续用,不会影响已有调用方。
// 好的接口定义
public interface UserService {
User getUserById(Long userId);
List<Order> findOrdersByStatus(OrderStatus status);
boolean createUser(UserCreateRequest request);
}
// 糟糕的接口定义
public interface UserService {
Object get(Long id); // 不知道返回什么
void save(Object obj); // 不知道保存什么
String doSomething(String s); // 不知道做什么
}
参数传递方式:少即是多
参数传递,我踩过最大的坑就是「参数膨胀」。
一开始接口只有两三个参数,后来业务需求越来越多,参数加到七八个。调用方每次都要传一堆 null,代码又臭又长。更可怕的是,参数顺序稍微一变,所有调用方都得改。
避坑指南:
我曾经接手过一个项目,有个接口的参数列表长达12个。每次调用都要对着文档数参数位置,数错一个就出bug。后来我重构时,把所有参数封装成一个请求对象,世界清净了。
我总结了几条参数传递的原则:
| 参数数量 | 推荐方式 | 说明 |
|---|---|---|
| 0-2个 | 直接传参 | 简单明了,无需封装 |
| 3-5个 | 封装成对象 | 用 Request/Param 对象包裹 |
| 5个以上 | 必须封装 | 否则就是灾难 |
// 不推荐:参数太多
public List<Order> searchOrders(Long userId, String status, Date startTime,
Date endTime, Integer page, Integer size,
String sortBy, Boolean asc) {
// ...
}
// 推荐:封装成对象
public List<Order> searchOrders(OrderSearchRequest request) {
// ...
}
// 请求对象
public class OrderSearchRequest {
private Long userId;
private String status;
private DateRange timeRange; // 把开始结束时间也封装了
private PageRequest page;
private SortRequest sort;
}
另外,布尔参数要慎用。你想想看,调用方看到 processOrder(true, false),鬼知道这两个布尔值是什么意思。我建议用枚举代替布尔值,代码可读性会好很多。
返回值设计:给调用方确定性
返回值设计,核心是「让调用方知道发生了什么」。
我见过最糟糕的返回值设计,就是返回 Object 或者 Map。调用方拿到结果后,还得猜里面有什么字段,猜错了就报空指针。这哪是接口,分明是猜谜游戏。
我的习惯:
所有接口都返回统一的结果对象。成功时带数据,失败时带错误码和错误信息。调用方只需要判断 result.isSuccess(),然后取数据就行。
// 统一返回结果
public class ApiResult<T> {
private boolean success;
private String code; // 业务错误码
private String message; // 错误描述
private T data; // 实际数据
// 成功
public static <T> ApiResult<T> success(T data) {
ApiResult<T> result = new ApiResult<>();
result.success = true;
result.data = data;
return result;
}
// 失败
public static <T> ApiResult<T> error(String code, String message) {
ApiResult<T> result = new ApiResult<>();
result.success = false;
result.code = code;
result.message = message;
return result;
}
}
// 调用方使用
ApiResult<User> result = userService.getUserById(123L);
if (result.isSuccess()) {
User user = result.getData();
// 处理业务
} else {
log.error("获取用户失败: {} - {}", result.getCode(), result.getMessage());
// 返回错误给前端
}
这里要注意:不要返回 null。空集合就返回空列表,空对象就返回空对象。调用方不用每次都做 null 判断,代码会干净很多。
异常处理策略:别让异常裸奔
异常处理,说白了就是「出了问题,谁来兜底」。
我在项目中遇到过一种情况:某个模块抛出了 NullPointerException,结果一路往上抛,到了最外层才被捕获。日志里只有一行 NullPointerException,连哪个接口、哪个参数导致的都不知道。排查起来简直要命。
我建议的异常处理策略:
- 自定义业务异常:定义
BusinessException,带上错误码和上下文信息 - 异常在模块边界捕获:模块内部不要吞异常,也不要裸抛
- 统一异常处理:在最外层(比如 Controller 层)统一捕获并转换
// 自定义业务异常
public class BusinessException extends RuntimeException {
private String code;
private String message;
private Map<String, Object> context; // 上下文信息,方便排查
public BusinessException(String code, String message) {
super(message);
this.code = code;
this.message = message;
}
// 可以带上上下文
public BusinessException withContext(String key, Object value) {
if (this.context == null) {
this.context = new HashMap<>();
}
this.context.put(key, value);
return this;
}
}
// 模块内部使用
public User getUserById(Long userId) {
User user = userDao.findById(userId);
if (user == null) {
throw new BusinessException("USER_NOT_FOUND", "用户不存在")
.withContext("userId", userId);
}
return user;
}
// 统一异常处理
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ApiResult<Void> handleBusinessException(BusinessException e) {
log.error("业务异常: {} - {}, context: {}",
e.getCode(), e.getMessage(), e.getContext());
return ApiResult.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
public ApiResult<Void> handleException(Exception e) {
log.error("系统异常", e);
return ApiResult.error("SYSTEM_ERROR", "系统繁忙,请稍后重试");
}
}
避坑指南:
我曾经见过一个团队,所有异常都抛 RuntimeException,然后在外层统一 catch 后返回「系统错误」。结果线上出了 bug,用户反馈说「系统错误」,开发查了半天日志,根本不知道是哪个模块出的问题。后来我强制要求:每个业务异常必须带错误码,而且错误码要能定位到具体模块和场景。
异常处理的另一个要点:区分业务异常和系统异常。业务异常(比如用户不存在、余额不足)应该返回给调用方明确的错误信息。系统异常(比如数据库连接失败、网络超时)应该记录详细日志,然后返回一个通用的错误提示。
嗯,这里要注意:不要用异常来控制业务流程。比如「用户不存在」这种情况,用返回值判断比用异常捕获更合适。异常是给「意外情况」准备的,不是给「正常业务分支」准备的。
总结一下,模块接口设计其实就四个要点:规范命名、精简参数、统一返回、优雅异常。把这四点做好了,你的模块接口就会像一扇干净的门,外面的人一看就知道怎么进,里面的人也知道怎么守。
可可精品资料库