DDD-025:API 设计与 Controller 层
本章导读
在 DDD 架构中,Controller 层作为系统的入口,承担着接收外部请求、协调应用服务、返回响应结果的重要职责。良好的 API 设计不仅能提升系统的可用性和可维护性,还能更好地体现领域模型的设计意图。本章将深入探讨如何在 DDD 架构中设计高质量的 RESTful API,以及 Controller 层的正确职责划分。
学习目标
前置知识
- DDD 分层架构基础
- HTTP 协议基础
- Spring MVC 基本使用
阅读时长
约 50-60 分钟
【原理】RESTful API 与 Controller 层设计原理
一、RESTful API 的本质与设计哲学
1.1 什么是 RESTful API
REST(Representational State Transfer,表述性状态转移)是一种软件架构风格,由 Roy Fielding 在 2000 年的博士论文中提出。RESTful API 是符合 REST 原则的 Web API 设计方式。
【原理】
REST 的核心原则:
传统 RPC 风格:
POST /api/createOrder
POST /api/deleteOrder
POST /api/getOrderById
RESTful 风格:
POST /api/orders # 创建订单
DELETE /api/orders/{id} # 删除订单
GET /api/orders/{id} # 查询订单
【历史架构问题】
问题 1:RPC 风格 API 的混乱
// ❌ 传统 RPC 风格:动词在 URL 中,语义不清
@RestController
@RequestMapping(\”/api\”)
public class OrderApiController {
@PostMapping(\”/createOrder\”)
public Result createOrder(@RequestBody CreateOrderRequest request) {
... }
@PostMapping(\”/deleteOrderById\”)
public Result deleteOrder(@RequestParam String orderId) {
... }
@PostMapping(\”/updateOrderStatus\”)
public Result updateStatus(@RequestBody UpdateStatusRequest request) {
... }
@PostMapping(\”/getOrderDetail\”)
public Result getOrderDetail(@RequestParam String orderId) {
... }
@PostMapping(\”/queryOrderList\”)
public Result queryOrderList(@RequestBody OrderQuery query) {
... }
}
问题分析:
- URL 命名不一致(createOrder、deleteOrderById、getOrderDetail)
- HTTP 方法使用混乱(全是 POST)
- 违反 REST 的统一接口原则
- 不利于缓存和中间件处理
问题 2:HTTP 方法滥用
// ❌ 错误:用 GET 执行删除操作
@GetMapping(\”/deleteOrder\”)
public Result deleteOrder(@RequestParam String id) {
orderService.delete(id);
return Result.success();
}
// ❌ 错误:用 POST 执行查询
@PostMapping(\”/queryOrders\”)
public Result queryOrders(@RequestBody OrderQuery query) {
return Result.success(orderService.query(query));
}
问题分析:
- GET 请求执行删除,可能被浏览器预加载误触发
- POST 查询无法被缓存,影响性能
- 违反 HTTP 方法语义,难以理解
问题 3:URL 设计不规范
// ❌ 错误:URL 设计混乱
@GetMapping(\”/order/detail\”) // 混合风格
@GetMapping(\”/orders/detail/{id}\”) // 不必要的层级
@GetMapping(\”/api/v1/order/get/{id}\”) // 动词污染
@GetMapping(\”/order-info/{orderId}\”) // 不一致的命名
【DDD 如何解决】
解决方案:遵循 RESTful 设计原则
// ✅ 正确:RESTful 风格 API
@RestController
@RequestMapping(\”/api/v1/orders\”)
public class OrderController {
@PostMapping
public ApiResponse<OrderId> createOrder(@Valid @RequestBody CreateOrderRequest request) {
// POST /api/v1/orders – 创建资源
}
@GetMapping(\”/{orderId}\”)
public ApiResponse<OrderResponse> getOrder(@PathVariable String orderId) {
// GET /api/v1/orders/{orderId} – 查询资源
}
@GetMapping
public ApiResponse<PageResponse<OrderResponse>> listOrders(OrderQueryRequest request) {
// GET /api/v1/orders – 列表查询
}
@PutMapping(\”/{orderId}/status\”)
public ApiResponse<Void> updateStatus(
@PathVariable String orderId,
@Valid @RequestBody UpdateStatusRequest request) {
// PUT /api/v1/orders/{orderId}/status – 更新资源
}
@DeleteMapping(\”/{orderId}\”)
public ApiResponse<Void> deleteOrder(@PathVariable String orderId) {
// DELETE /api/v1/orders/{orderId} – 删除资源
}
}
设计要点:
| 资源命名 | 使用名词复数 | /orders, /products |
| HTTP 方法 | 遵循语义 | GET 查询、POST 创建、PUT 更新、DELETE 删除 |
| 层级清晰 | 表达资源关系 | /orders/{id}/items |
| 版本控制 | URL 或 Header | /api/v1/orders |
| 统一响应 | 标准响应格式 | ApiResponse |
【设计优势】
| 语义清晰度 | 低,需要看文档 | 高,URL 即文档 |
| HTTP 语义 | 混乱 | 标准,符合规范 |
| 缓存友好 | 差(全 POST) | 好(GET 可缓存) |
| 工具支持 | 需要定制 | 标准工具支持 |
| 学习成本 | 每个接口都要学 | 遵循统一规范 |
1.2 HTTP 方法的正确使用
【原理】
HTTP 方法定义了对资源的操作语义:
| GET | 获取资源 | 是 | 是 | 查询 |
| POST | 创建资源 | 否 | 否 | 新增、复杂操作 |
| PUT | 全量更新 | 是 | 否 | 整体替换 |
| PATCH | 部分更新 | 否 | 否 | 部分修改 |
| DELETE | 删除资源 | 是 | 否 | 删除 |
幂等性(Idempotent):多次执行相同请求,结果与执行一次相同。 安全性(Safe):请求不会改变服务器状态。
【代码示例】
// ✅ 正确:遵循 HTTP 方法语义
// GET – 获取订单(安全、幂等)
@GetMapping(\”/{orderId}\”)
public ApiResponse<OrderResponse> getOrder(@PathVariable String orderId) {
GetOrderQuery query = new GetOrderQuery(OrderId.of(orderId));
OrderResponse response = orderQueryService.getOrder(query);
return ApiResponse.success(response);
}
// POST – 创建订单(非幂等)
@PostMapping
public ApiResponse<OrderId> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
CreateOrderCommand command = OrderAssembler.toCommand(request);
OrderId orderId = orderApplicationService.createOrder(command);
return ApiResponse.success(orderId);
}
// PUT – 全量更新订单地址(幂等)
@PutMapping(\”/{orderId}/shipping-address\”)
public ApiResponse<Void> updateShippingAddress(
@PathVariable String orderId,
@Valid @RequestBody UpdateAddressRequest request) {
UpdateAddressCommand command = new UpdateAddressCommand(
OrderId.of(orderId),
request.getAddress()
);
orderApplicationService.updateShippingAddress(command);
return ApiResponse.success();
}
// PATCH – 部分更新订单备注(非幂等)
@PatchMapping(\”/{orderId}\”)
public ApiResponse<Void> patchOrder(
@PathVariable String orderId,
@RequestBody Map<String, Object> updates) {
// 只更新指定字段
orderApplicationService.patchOrder(OrderId.of(orderId), updates);
return ApiResponse.success();
}
// DELETE – 取消订单(幂等)
@DeleteMapping(\”/{orderId}\”)
public ApiResponse<Void> cancelOrder(
@PathVariable String orderId,
@RequestParam(required = false) String reason) {
CancelOrderCommand command = new CancelOrderCommand(
OrderId.of(orderId),
reason
);
orderApplicationService.cancelOrder(command);
return ApiResponse.success();
}
// POST – 执行复杂业务操作(非 CRUD 操作)
@PostMapping(\”/{orderId}/payment\”)
public ApiResponse<PaymentResult> payOrder(
@PathVariable String orderId,
@Valid @RequestBody PayOrderRequest request) {
PayOrderCommand command = new PayOrderCommand(
OrderId.of(orderId),
request.getPaymentMethod()
);
PaymentResult result = orderApplicationService.payOrder(command);
return ApiResponse.success(result);
}
1.3 URL 设计规范
【原理】
URL 设计原则:
推荐格式:
/api/{version}/{resource}/{id}/{sub-resource}/{sub-id}
示例:
/api/v1/orders # 订单集合
/api/v1/orders/{orderId} # 单个订单
/api/v1/orders/{orderId}/items # 订单项集合
/api/v1/orders/{orderId}/items/{itemId} # 单个订单项
/api/v1/customers/{customerId}/orders # 客户的订单
【代码示例】
// ✅ 正确:规范的 URL 设计
@RestController
@RequestMapping(\”/api/v1/orders\”)
public class OrderController {
// 订单集合操作
@GetMapping
public ApiResponse<PageResponse<OrderSummary>> listOrders(
@RequestParam(required = false) String customerId,
@RequestParam(required = false) String status,
@RequestParam(defaultValue = \”0\”) int page,
@RequestParam(defaultValue = \”20\”) int size) {
// GET /api/v1/orders?customerId=xxx&status=CREATED&page=0&size=20
}
// 单个订单操作
@GetMapping(\”/{orderId}\”)
public ApiResponse<OrderDetail> getOrder(@PathVariable String orderId) {
// GET /api/v1/orders/ORD-001
}
// 订单项子资源
@GetMapping(\”/{orderId}/items\”)
public ApiResponse<List<OrderItemResponse>> getOrderItems(
@PathVariable String orderId) {
// GET /api/v1/orders/ORD-001/items
}
// 订单项详情
@GetMapping(\”/{orderId}/items/{itemId}\”)
public ApiResponse<OrderItemResponse> getOrderItem(
@PathVariable String orderId,
@PathVariable String itemId) {
// GET /api/v1/orders/ORD-001/items/ITEM-001
}
}
// 另一个资源的控制器
@RestController
@RequestMapping(\”/api/v1/customers\”)
public class CustomerController {
// 客户的订单(通过关系访问)
@GetMapping(\”/{customerId}/orders\”)
public ApiResponse<List<OrderSummary>> getCustomerOrders(
@PathVariable String customerId,
@RequestParam(required = false) String status) {
// GET /api/v1/customers/CUST-001/orders?status=PAID
}
}
二、Controller 层的职责边界
2.1 Controller 在 DDD 分层中的位置
【原理】
在 DDD 分层架构中,Controller 属于接口层(Interface Layer / User Interface Layer),也称为表示层。
┌─────────────────────────────────────────────────────────┐
│ 接口层(Interface Layer) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Controller │ │ Assembler │ │ DTO │ │
│ │ (请求处理) │ │ (对象转换) │ │ (数据传输) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ 调用
┌─────────────────────────────────────────────────────────┐
│ 应用层(Application Layer) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Application Service (用例编排) │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ 调用
┌─────────────────────────────────────────────────────────┐
│ 领域层(Domain Layer) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Entity │ │ Value │ │ Service │ │Repository│ │
│ │ │ │ Object │ │ │ │ Interface│ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────┘
Controller 的职责:
Controller 不应该做的事:
【历史架构问题】
问题 1:Controller 承担过多职责
// ❌ 错误:Controller 包含业务逻辑
@RestController
@RequestMapping(\”/api/orders\”)
public class BadOrderController {
@Autowired
private OrderDao orderDao;
@Autowired
private ProductDao productDao;
@Autowired
private CustomerDao customerDao;
@PostMapping
public Result createOrder(@RequestBody CreateOrderDTO dto) {
// ❌ Controller 直接操作数据库
Customer customer = customerDao.findById(dto.getCustomerId());
if (customer == null) {
return Result.error(\”客户不存在\”);
}
// ❌ Controller 包含业务逻辑
List<OrderItem> items = new ArrayList<>();
BigDecimal totalAmount = BigDecimal.ZERO;
for (OrderItemDTO itemDTO : dto.getItems()) {
Product product = productDao.findById(itemDTO.getProductId());
if (product == null) {
return Result.error(\”商品不存在:\” + itemDTO.getProductId());
}
// ❌ 库存检查逻辑在 Controller
if (product.getStock() < itemDTO.getQuantity()) {
return Result.error(\”库存不足:\” + product.getName());
}
// ❌ 价格计算逻辑在 Controller
BigDecimal subtotal = product.getPrice().multiply(
new BigDecimal(itemDTO.getQuantity()));
totalAmount = totalAmount.add(subtotal);
OrderItem item = new OrderItem();
item.setProductId(product.getId());
item.setProductName(product.getName());
item.setQuantity(itemDTO.getQuantity());
item.setPrice(product.getPrice());
items.add(item);
}
// ❌ 订单创建逻辑在 Controller
Order order = new Order();
order.setId(UUID.randomUUID().toString());
order.setCustomerId(dto.getCustomerId());
order.setCustomerName(customer.getName());
order.setItems(items);
order.setTotalAmount(totalAmount);
order.setStatus(\”CREATED\”);
order.setCreatedAt(LocalDateTime.now());
// ❌ 直接保存到数据库
orderDao.insert(order);
return Result.success(order.getId());
}
}
问题分析:
问题 2:Controller 直接操作领域对象
// ❌ 错误:Controller 直接操作聚合
@PostMapping(\”/{orderId}/payment\”)
public Result payOrder(@PathVariable String orderId, @RequestBody PayOrderDTO dto) {
// ❌ 直接获取聚合
OrderEntity entity = orderDao.findById(orderId);
// ❌ 直接修改聚合状态
if (!\”CREATED\”.equals(entity.getStatus())) {
return Result.error(\”订单状态不正确\”);
}
// ❌ 在 Controller 中调用外部支付服务
PaymentResult result = paymentService.pay(entity.getTotalAmount(), dto.getPaymentMethod());
if (result.isSuccess()) {
entity.setStatus(\”PAID\”);
entity.setPaymentId(result.getPaymentId());
entity.setPaidAt(LocalDateTime.now());
orderDao.update(entity);
// ❌ 发送消息在 Controller
messageQueue.send(new OrderPaidMessage(orderId));
}
return Result.success(result);
}
问题分析:
【DDD 如何解决】
解决方案:Controller 只做协调工作
// ✅ 正确:Controller 职责单一
@RestController
@RequestMapping(\”/api/v1/orders\”)
@Validated
public class OrderController {
private final OrderApplicationService orderApplicationService;
private final OrderQueryService orderQueryService;
private final OrderAssembler orderAssembler;
public OrderController(
OrderApplicationService orderApplicationService,
OrderQueryService orderQueryService,
OrderAssembler orderAssembler) {
this.orderApplicationService = orderApplicationService;
this.orderQueryService = orderQueryService;
this.orderAssembler = orderAssembler;
}
/**
* 创建订单
* Controller 只负责:接收请求 -> 参数校验 -> 调用应用服务 -> 返回响应
*/
@PostMapping
public ApiResponse



