接口颗粒度设计方法

#设计 #最佳实践 #接口

总结
  • 接口颗粒度核心问题:不同调用方需要的字段组合各不相同,怎么设计不接口爆炸
  • 三种方案:稀疏字段集(改造成本低)、BFF(多端差异大)、GraphQL(字段组合复杂)
  • 不推荐"三粒度分级",边界模糊、DTO 重叠、调用方还是要猜

1. 先说结论

接口颗粒度的核心问题是:一个业务对象字段很多,不同调用方需要的字段组合各不相同,怎么设计才不会接口爆炸?

三种方案,按推荐程度排:

方案 适用场景 复杂度
稀疏字段集(?fields= REST 风格,改造成本低
BFF(Backend for Frontend) 多端差异大,微服务架构
GraphQL 字段组合复杂,调用方多样

2. 稀疏字段集(Sparse Fieldsets)

REST 下最简单的折中方案。一个接口,调用方通过 fields 参数声明要哪些字段,服务端按需返回。

GET /orders/123?fields=orderId,status,amount
GET /orders/123?fields=orderId,status,amount,userName,userPhone,products
GET /orders/123  // 不传则返回完整数据

2.1 服务端怎么实现?

@GetMapping("/orders/{id}")
public OrderDTO getOrder(@PathVariable String id,
                         @RequestParam(required = false) Set<String> fields) {
    OrderDTO order = orderService.getOrder(id);
    if (fields == null || fields.isEmpty()) {
        return order;
    }
    // 按 fields 过滤,只返回调用方需要的字段
    return FieldFilter.filter(order, fields);
}

FieldFilter 可以用反射或 Jackson 的 @JsonFilter 实现:

// 用 Jackson FilterProvider 动态过滤字段
ObjectMapper mapper = new ObjectMapper();
FilterProvider filters = new SimpleFilterProvider()
    .addFilter("fieldFilter", SimpleBeanPropertyFilter.filterOutAllExcept(fields));
mapper.writer(filters).writeValueAsString(order);

2.2 优缺点

优点

缺点


3. BFF(Backend for Frontend)

不同端(移动端、PC、第三方)各自有一个聚合层,按端的需求裁剪和聚合数据,核心服务只提供原子接口。

移动端 App  →  Mobile BFF  ─┐
PC 管理后台  →  Admin BFF   ─┤→  订单服务 / 用户服务 / 商品服务
第三方平台   →  Open BFF   ─┘

3.1 核心服务只做原子接口

// 订单服务:只返回订单自身的数据
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable String id);

// 用户服务:只返回用户数据
@GetMapping("/users/{id}")
public User getUser(@PathVariable String id);

3.2 BFF 层按端聚合

// Mobile BFF:移动端只需要简要信息
@GetMapping("/mobile/orders/{id}")
public MobileOrderVO getOrder(@PathVariable String id) {
    Order order = orderClient.getOrder(id);
    User user = userClient.getUser(order.getUserId());
    
    // 只组装移动端需要的字段
    return MobileOrderVO.builder()
        .orderId(order.getId())
        .status(order.getStatus())
        .amount(order.getAmount())
        .userName(user.getName())
        .build();
}

// Admin BFF:后台需要完整信息
@GetMapping("/admin/orders/{id}")
public AdminOrderVO getOrder(@PathVariable String id) {
    Order order = orderClient.getOrder(id);
    User user = userClient.getUser(order.getUserId());
    List<Product> products = productClient.getByOrderId(id);
    List<Log> logs = logClient.getByOrderId(id);
    
    return AdminOrderVO.assemble(order, user, products, logs);
}

3.3 优缺点

优点

缺点


4. GraphQL

让调用方自己声明要哪些字段,服务端按声明精确返回,从根本上解决字段组合爆炸的问题。

4.1 调用方按需查询

# 列表页:只要基本字段
query {
  order(id: "123") {
    orderId
    status
    amount
  }
}

# 详情页:要更多字段
query {
  order(id: "123") {
    orderId
    status
    amount
    user {
      name
      phone
    }
    products {
      name
      quantity
      price
    }
  }
}

4.2 服务端定义 Schema

type Order {
  orderId: ID!
  status: String!
  amount: Float!
  createTime: String!
  user: User
  products: [Product]
  paymentInfo: PaymentInfo
  logs: [OrderLog]
}

type Query {
  order(id: ID!): Order
  orders(userId: ID, status: String): [Order]
}

4.3 Resolver 按需加载

@Component
public class OrderResolver implements GraphQLQueryResolver {
    
    public Order order(String id) {
        // 只查订单基本信息
        return orderService.getOrder(id);
    }
}

@Component
public class OrderFieldResolver implements GraphQLResolver<Order> {
    
    // 只有调用方查询了 user 字段,这个方法才会被调用
    public User user(Order order) {
        return userService.getUser(order.getUserId());
    }
    
    // 只有查询了 products 字段才加载
    public List<Product> products(Order order) {
        return productService.getByOrderId(order.getId());
    }
}

4.4 优缺点

优点

缺点


5. 怎么选?

用稀疏字段集

用 BFF

用 GraphQL

不推荐"三粒度接口"
按字段数量切接口(粗/中/细)是一种工程妥协,边界模糊、DTO 重叠、调用方还是要猜该用哪个。如果场景固定、团队规模小,短期内可以用,但不是长期方案。