模块接口设计:接口定义规范、参数传递方式、返回值设计、异常处理策略

模块接口设计,说白了就是给模块画个「门面」。

你想想看,一个系统里几十上百个模块,大家怎么协作?靠的就是接口。接口设计得好,模块之间就像乐高积木,随便拼。设计得不好,那就是一团乱麻,改一个地方崩一片。

我个人习惯,在开始写任何模块代码之前,先把接口定死。接口不动,内部随便重构。这就像盖楼先画图纸,图纸定了,施工队才能进场。

接口定义规范:先定规矩,再干活

接口定义,核心就四个字:清晰、稳定。

我在项目中遇到过最头疼的事,就是接口命名混乱。有人用 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,用户反馈说「系统错误」,开发查了半天日志,根本不知道是哪个模块出的问题。后来我强制要求:每个业务异常必须带错误码,而且错误码要能定位到具体模块和场景。

异常处理的另一个要点:区分业务异常和系统异常。业务异常(比如用户不存在、余额不足)应该返回给调用方明确的错误信息。系统异常(比如数据库连接失败、网络超时)应该记录详细日志,然后返回一个通用的错误提示。

嗯,这里要注意:不要用异常来控制业务流程。比如「用户不存在」这种情况,用返回值判断比用异常捕获更合适。异常是给「意外情况」准备的,不是给「正常业务分支」准备的。

模块接口设计核心知识体系 模块接口设计 接口定义规范 参数传递方式 返回值设计 异常处理策略 命名规范 版本管理 直接传参 对象封装 统一结果 避免null 自定义异常 统一处理

总结一下,模块接口设计其实就四个要点:规范命名、精简参数、统一返回、优雅异常。把这四点做好了,你的模块接口就会像一扇干净的门,外面的人一看就知道怎么进,里面的人也知道怎么守。

可可精品资料库