1. 为什么要做对象分层?
对象分层的本质不是"类越多越高级",
而是为了把不同类型的变化隔离开。
实际开发里,变化大致有 4 类:
- 输入变化(前端新增字段、校验规则变化)
- 业务变化(折扣策略、计算规则、编排流程变化)
- 存储变化(表字段调整、索引策略变化)
- 展示变化(页面新增字段、展示格式/脱敏变化)
如果你只用一个大对象或者直接把 Entity 暴露到前端:
- UI 变化会逼你改数据库对象
- 规则变化会把入参对象污染成"临时字段垃圾桶"
- 数据结构变化会反向破坏接口稳定性
正确分层的价值就是:
让变化有"正确的承接层"。
2. 最少概念(够用版)
记住一句话就够:
DTO 进 → BO 算 → Entity 存 → VO 出
对应职责:
- Command DTO:新增/修改/提交的入参
- Query DTO:列表/分页/筛选的入参
- BO:Service 内部的业务计算/编排中间态
- Entity/DO:数据库表映射对象
- VO(View Object):返回给前端的展示对象
- Converter:把"对象转换"集中起来,避免层与层乱引用
你会发现这套体系的核心不是名字,
而是"输入、计算、存储、展示"四类语义的拆分。
3. 什么时候一定要引入 BO?
很多人最大的犹豫是:
BO 是不是"多余的一层"?
判断很简单:
3.1 不用 BO 也可以的场景
- 纯 CRUD
- 基本无规则计算
- 只是简单表单提交
这时你用:
DTO + Entity + VO + Converter
就足够了。
3.2 强烈建议使用 BO 的场景
- 价格/薪资/计费/清结算
- 多数据源聚合
- 规则叠加、计算链长
- 同一套计算逻辑需要复用(预览/下单/退款反算)
因为 BO 的本质是:
业务计算的"中间态容器"。
它能防止:
- DTO 被计算字段污染
- Entity 被业务临时状态污染
- Controller 变成"规则堆叠现场"
4. 推荐目录结构
text
com.example.order
├── controller
├── service
├── dto
│ ├── command
│ └── query
├── bo
├── entity
├── vo
├── converter
└── mapper
这个结构的意义很朴素:
你看到包名就知道对象的生命周期与边界。
5. 案例目标与链路
我们用一个简化订单模块来跑通关键链路:
5.1 需求
- 价格预览
- 确认下单
- 订单分页查询
5.2 链路设计
-
价格预览:
PlaceOrderCommandDTO- →
OrderPriceBO(完成规则计算) - →
OrderPreviewVO
-
下单:
PlaceOrderCommandDTO- →
OrderPriceBO - →
OrderEntity + OrderItemEntity入库
-
分页:
OrderPageQueryDTO- → Mapper 查询
OrderEntity - → 转
OrderListVO+PageResult
这样的结构保证:
- 预览和下单复用同一套计算逻辑
- 计算逻辑不污染前端入参
- 展示变化不倒灌数据库对象
6. 完整代码示例(带解释)
下面代码是一个"最小可落地模板"。
真实项目中你只需要把"模拟价格/优惠券逻辑"替换为真实查询与规则引擎即可。
6.1 Command DTO(下单入参)
为什么是 Command?
强调它表达的是"动作意图",不是"数据库结构"。
java
package com.example.order.dto.command;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;
public class PlaceOrderCommandDTO {
@NotNull
private Long userId;
@NotEmpty
private List<Item> items;
private Long couponId; // 可选
public static class Item {
@NotNull
private Long skuId;
@NotNull
private Integer qty;
public Long getSkuId() { return skuId; }
public void setSkuId(Long skuId) { this.skuId = skuId; }
public Integer getQty() { return qty; }
public void setQty(Integer qty) { this.qty = qty; }
}
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public List<Item> getItems() { return items; }
public void setItems(List<Item> items) { this.items = items; }
public Long getCouponId() { return couponId; }
public void setCouponId(Long couponId) { this.couponId = couponId; }
}
6.2 Query DTO(分页查询入参)
Query DTO 的价值 在于:
分页、筛选字段不应该污染命令入参对象,更不应该去影响 Entity。
java
package com.example.order.dto.query;
public class OrderPageQueryDTO {
private Long userId;
private Integer status;
private Integer pageNum = 1;
private Integer pageSize = 20;
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
public Integer getPageNum() { return pageNum; }
public void setPageNum(Integer pageNum) { this.pageNum = pageNum; }
public Integer getPageSize() { return pageSize; }
public void setPageSize(Integer pageSize) { this.pageSize = pageSize; }
}
6.3 BO(价格计算中间态)
这就是本例的"核心价值层"。
你会看到:所有计算结果与中间状态都留在 BO ,
这样 DTO 和 Entity 都能保持干净。
java
package com.example.order.bo;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class OrderPriceBO {
private Long userId;
private Long couponId;
private List<Line> lines = new ArrayList<>();
private BigDecimal originAmount = BigDecimal.ZERO;
private BigDecimal couponDiscount = BigDecimal.ZERO;
private BigDecimal freight = BigDecimal.ZERO;
private BigDecimal tax = BigDecimal.ZERO;
private BigDecimal payAmount = BigDecimal.ZERO;
public static class Line {
private Long skuId;
private Integer qty;
private BigDecimal unitPrice = BigDecimal.ZERO;
private BigDecimal lineAmount = BigDecimal.ZERO;
public Long getSkuId() { return skuId; }
public void setSkuId(Long skuId) { this.skuId = skuId; }
public Integer getQty() { return qty; }
public void setQty(Integer qty) { this.qty = qty; }
public BigDecimal getUnitPrice() { return unitPrice; }
public void setUnitPrice(BigDecimal unitPrice) { this.unitPrice = unitPrice; }
public BigDecimal getLineAmount() { return lineAmount; }
public void setLineAmount(BigDecimal lineAmount) { this.lineAmount = lineAmount; }
}
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public Long getCouponId() { return couponId; }
public void setCouponId(Long couponId) { this.couponId = couponId; }
public List<Line> getLines() { return lines; }
public void setLines(List<Line> lines) { this.lines = lines; }
public BigDecimal getOriginAmount() { return originAmount; }
public void setOriginAmount(BigDecimal originAmount) { this.originAmount = originAmount; }
public BigDecimal getCouponDiscount() { return couponDiscount; }
public void setCouponDiscount(BigDecimal couponDiscount) { this.couponDiscount = couponDiscount; }
public BigDecimal getFreight() { return freight; }
public void setFreight(BigDecimal freight) { this.freight = freight; }
public BigDecimal getTax() { return tax; }
public void setTax(BigDecimal tax) { this.tax = tax; }
public BigDecimal getPayAmount() { return payAmount; }
public void setPayAmount(BigDecimal payAmount) { this.payAmount = payAmount; }
}
6.4 Entity(落库对象)
Entity 的第一原则:
贴表结构、少业务表达、不要面向 UI。
java
package com.example.order.entity;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class OrderEntity {
private Long id;
private String orderNo;
private Long userId;
private Integer status;
private BigDecimal originAmount;
private BigDecimal couponDiscount;
private BigDecimal freight;
private BigDecimal tax;
private BigDecimal payAmount;
private LocalDateTime createTime;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getOrderNo() { return orderNo; }
public void setOrderNo(String orderNo) { this.orderNo = orderNo; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
public BigDecimal getOriginAmount() { return originAmount; }
public void setOriginAmount(BigDecimal originAmount) { this.originAmount = originAmount; }
public BigDecimal getCouponDiscount() { return couponDiscount; }
public void setCouponDiscount(BigDecimal couponDiscount) { this.couponDiscount = couponDiscount; }
public BigDecimal getFreight() { return freight; }
public void setFreight(BigDecimal freight) { this.freight = freight; }
public BigDecimal getTax() { return tax; }
public void setTax(BigDecimal tax) { this.tax = tax; }
public BigDecimal getPayAmount() { return payAmount; }
public void setPayAmount(BigDecimal payAmount) { this.payAmount = payAmount; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
}
java
package com.example.order.entity;
import java.math.BigDecimal;
public class OrderItemEntity {
private Long id;
private Long orderId;
private Long skuId;
private Integer qty;
private BigDecimal unitPrice;
private BigDecimal lineAmount;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getOrderId() { return orderId; }
public void setOrderId(Long orderId) { this.orderId = orderId; }
public Long getSkuId() { return skuId; }
public void setSkuId(Long skuId) { this.skuId = skuId; }
public Integer getQty() { return qty; }
public void setQty(Integer qty) { this.qty = qty; }
public BigDecimal getUnitPrice() { return unitPrice; }
public void setUnitPrice(BigDecimal unitPrice) { this.unitPrice = unitPrice; }
public BigDecimal getLineAmount() { return lineAmount; }
public void setLineAmount(BigDecimal lineAmount) { this.lineAmount = lineAmount; }
}
6.5 VO(对外展示对象)
VO 的第一原则:
为页面/接口服务,允许组合字段、文案字段、格式化字段。
java
package com.example.order.vo;
import java.math.BigDecimal;
import java.util.List;
public class OrderPreviewVO {
private BigDecimal originAmount;
private BigDecimal couponDiscount;
private BigDecimal freight;
private BigDecimal tax;
private BigDecimal payAmount;
private List<Line> lines;
public static class Line {
private Long skuId;
private Integer qty;
private BigDecimal unitPrice;
private BigDecimal lineAmount;
public Long getSkuId() { return skuId; }
public void setSkuId(Long skuId) { this.skuId = skuId; }
public Integer getQty() { return qty; }
public void setQty(Integer qty) { this.qty = qty; }
public BigDecimal getUnitPrice() { return unitPrice; }
public void setUnitPrice(BigDecimal unitPrice) { this.unitPrice = unitPrice; }
public BigDecimal getLineAmount() { return lineAmount; }
public void setLineAmount(BigDecimal lineAmount) { this.lineAmount = lineAmount; }
}
public BigDecimal getOriginAmount() { return originAmount; }
public void setOriginAmount(BigDecimal originAmount) { this.originAmount = originAmount; }
public BigDecimal getCouponDiscount() { return couponDiscount; }
public void setCouponDiscount(BigDecimal couponDiscount) { this.couponDiscount = couponDiscount; }
public BigDecimal getFreight() { return freight; }
public void setFreight(BigDecimal freight) { this.freight = freight; }
public BigDecimal getTax() { return tax; }
public void setTax(BigDecimal tax) { this.tax = tax; }
public BigDecimal getPayAmount() { return payAmount; }
public void setPayAmount(BigDecimal payAmount) { this.payAmount = payAmount; }
public List<Line> getLines() { return lines; }
public void setLines(List<Line> lines) { this.lines = lines; }
}
java
package com.example.order.vo;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class OrderListVO {
private Long id;
private String orderNo;
private Integer status;
private String statusText;
private BigDecimal payAmount;
private LocalDateTime createTime;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getOrderNo() { return orderNo; }
public void setOrderNo(String orderNo) { this.orderNo = orderNo; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
public String getStatusText() { return statusText; }
public void setStatusText(String statusText) { this.statusText = statusText; }
public BigDecimal getPayAmount() { return payAmount; }
public void setPayAmount(BigDecimal payAmount) { this.payAmount = payAmount; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
}
6.6 PageResult(通用分页封装)
分页对象不是必须,但做过中台的人都知道它有多省心:
java
package com.example.common;
import java.util.List;
public class PageResult<T> {
private List<T> records;
private long total;
private int pageNum;
private int pageSize;
public static <T> PageResult<T> of(List<T> records, long total, int pageNum, int pageSize) {
PageResult<T> p = new PageResult<>();
p.records = records;
p.total = total;
p.pageNum = pageNum;
p.pageSize = pageSize;
return p;
}
public List<T> getRecords() { return records; }
public long getTotal() { return total; }
public int getPageNum() { return pageNum; }
public int getPageSize() { return pageSize; }
}
6.7 Converter(保证边界不乱)
很多团队对象分层失败的核心原因只有一个:
转换逻辑散落在 Controller/Service/Mapper 的各个角落。
Converter 就是来解决这个问题的。
java
package com.example.order.converter;
import com.example.order.bo.OrderPriceBO;
import com.example.order.dto.command.PlaceOrderCommandDTO;
import com.example.order.entity.OrderEntity;
import com.example.order.entity.OrderItemEntity;
import com.example.order.vo.OrderListVO;
import com.example.order.vo.OrderPreviewVO;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
public class OrderConverter {
public static OrderPriceBO toPriceBO(PlaceOrderCommandDTO dto) {
OrderPriceBO bo = new OrderPriceBO();
bo.setUserId(dto.getUserId());
bo.setCouponId(dto.getCouponId());
List<OrderPriceBO.Line> lines = dto.getItems().stream().map(i -> {
OrderPriceBO.Line l = new OrderPriceBO.Line();
l.setSkuId(i.getSkuId());
l.setQty(i.getQty());
return l;
}).collect(Collectors.toList());
bo.setLines(lines);
return bo;
}
public static OrderPreviewVO toPreviewVO(OrderPriceBO bo) {
OrderPreviewVO vo = new OrderPreviewVO();
vo.setOriginAmount(bo.getOriginAmount());
vo.setCouponDiscount(bo.getCouponDiscount());
vo.setFreight(bo.getFreight());
vo.setTax(bo.getTax());
vo.setPayAmount(bo.getPayAmount());
List<OrderPreviewVO.Line> lines = bo.getLines().stream().map(l -> {
OrderPreviewVO.Line v = new OrderPreviewVO.Line();
v.setSkuId(l.getSkuId());
v.setQty(l.getQty());
v.setUnitPrice(l.getUnitPrice());
v.setLineAmount(l.getLineAmount());
return v;
}).collect(Collectors.toList());
vo.setLines(lines);
return vo;
}
public static OrderEntity toOrderEntity(OrderPriceBO bo, String orderNo) {
OrderEntity e = new OrderEntity();
e.setOrderNo(orderNo);
e.setUserId(bo.getUserId());
e.setStatus(0);
e.setOriginAmount(bo.getOriginAmount());
e.setCouponDiscount(bo.getCouponDiscount());
e.setFreight(bo.getFreight());
e.setTax(bo.getTax());
e.setPayAmount(bo.getPayAmount());
e.setCreateTime(LocalDateTime.now());
return e;
}
public static List<OrderItemEntity> toItemEntities(OrderPriceBO bo, Long orderId) {
return bo.getLines().stream().map(l -> {
OrderItemEntity it = new OrderItemEntity();
it.setOrderId(orderId);
it.setSkuId(l.getSkuId());
it.setQty(l.getQty());
it.setUnitPrice(l.getUnitPrice());
it.setLineAmount(l.getLineAmount());
return it;
}).collect(Collectors.toList());
}
public static OrderListVO toListVO(OrderEntity e) {
OrderListVO vo = new OrderListVO();
vo.setId(e.getId());
vo.setOrderNo(e.getOrderNo());
vo.setStatus(e.getStatus());
vo.setPayAmount(e.getPayAmount());
vo.setCreateTime(e.getCreateTime());
vo.setStatusText(statusText(e.getStatus()));
return vo;
}
private static String statusText(Integer s) {
if (s == null) return "未知";
return switch (s) {
case 0 -> "待支付";
case 1 -> "已支付";
case 2 -> "已取消";
default -> "未知";
};
}
}
6.8 Mapper(只处理 Entity)
这是持久层的"边界红线"。
java
package com.example.order.mapper;
import com.example.order.entity.OrderEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface OrderMapper {
int insert(OrderEntity e);
List<OrderEntity> page(@Param("userId") Long userId,
@Param("status") Integer status,
@Param("offset") int offset,
@Param("limit") int limit);
long count(@Param("userId") Long userId,
@Param("status") Integer status);
}
java
package com.example.order.mapper;
import com.example.order.entity.OrderItemEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface OrderItemMapper {
int batchInsert(@Param("items") List<OrderItemEntity> items);
}
6.9 Service(BO 承担计算)
java
package com.example.order.service;
import com.example.common.PageResult;
import com.example.order.dto.command.PlaceOrderCommandDTO;
import com.example.order.dto.query.OrderPageQueryDTO;
import com.example.order.vo.OrderListVO;
import com.example.order.vo.OrderPreviewVO;
public interface OrderService {
OrderPreviewVO preview(PlaceOrderCommandDTO dto);
Long placeOrder(PlaceOrderCommandDTO dto);
PageResult<OrderListVO> page(OrderPageQueryDTO query);
}
java
package com.example.order.service.impl;
import com.example.common.PageResult;
import com.example.order.bo.OrderPriceBO;
import com.example.order.converter.OrderConverter;
import com.example.order.dto.command.PlaceOrderCommandDTO;
import com.example.order.dto.query.OrderPageQueryDTO;
import com.example.order.entity.OrderEntity;
import com.example.order.entity.OrderItemEntity;
import com.example.order.mapper.OrderItemMapper;
import com.example.order.mapper.OrderMapper;
import com.example.order.service.OrderService;
import com.example.order.vo.OrderListVO;
import com.example.order.vo.OrderPreviewVO;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class OrderServiceImpl implements OrderService {
private final OrderMapper orderMapper;
private final OrderItemMapper itemMapper;
public OrderServiceImpl(OrderMapper orderMapper, OrderItemMapper itemMapper) {
this.orderMapper = orderMapper;
this.itemMapper = itemMapper;
}
@Override
public OrderPreviewVO preview(PlaceOrderCommandDTO dto) {
OrderPriceBO bo = OrderConverter.toPriceBO(dto);
pricing(bo);
return OrderConverter.toPreviewVO(bo);
}
@Override
@Transactional
public Long placeOrder(PlaceOrderCommandDTO dto) {
OrderPriceBO bo = OrderConverter.toPriceBO(dto);
pricing(bo);
String orderNo = genOrderNo();
OrderEntity order = OrderConverter.toOrderEntity(bo, orderNo);
orderMapper.insert(order); // 依赖 MyBatis 主键回填
Long orderId = order.getId();
List<OrderItemEntity> items = OrderConverter.toItemEntities(bo, orderId);
itemMapper.batchInsert(items);
return orderId;
}
@Override
public PageResult<OrderListVO> page(OrderPageQueryDTO query) {
int offset = (query.getPageNum() - 1) * query.getPageSize();
List<OrderEntity> list = orderMapper.page(
query.getUserId(),
query.getStatus(),
offset,
query.getPageSize()
);
long total = orderMapper.count(query.getUserId(), query.getStatus());
List<OrderListVO> vos = list.stream()
.map(OrderConverter::toListVO)
.collect(Collectors.toList());
return PageResult.of(vos, total, query.getPageNum(), query.getPageSize());
}
/**
* 演示版价格计算:
* 真实项目替换为:
* - 查询 SKU 真实价格
* - 查询优惠券规则
* - 运费/税费策略
*/
private void pricing(OrderPriceBO bo) {
// 1) 演示:每个 sku 单价 100
for (OrderPriceBO.Line line : bo.getLines()) {
BigDecimal unit = BigDecimal.valueOf(100);
line.setUnitPrice(unit);
line.setLineAmount(unit.multiply(BigDecimal.valueOf(line.getQty())));
}
// 2) 原价
BigDecimal origin = bo.getLines().stream()
.map(OrderPriceBO.Line::getLineAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
bo.setOriginAmount(origin);
// 3) 优惠券:满 200 减 20
BigDecimal coupon = BigDecimal.ZERO;
if (bo.getCouponId() != null && origin.compareTo(BigDecimal.valueOf(200)) >= 0) {
coupon = BigDecimal.valueOf(20);
}
bo.setCouponDiscount(coupon);
// 4) 运费:原价 < 200 收 10
BigDecimal freight = origin.compareTo(BigDecimal.valueOf(200)) < 0
? BigDecimal.valueOf(10)
: BigDecimal.ZERO;
bo.setFreight(freight);
// 5) 税费:2%
BigDecimal tax = origin.multiply(BigDecimal.valueOf(0.02));
bo.setTax(tax);
// 6) 应付
BigDecimal pay = origin.subtract(coupon).add(freight).add(tax);
bo.setPayAmount(pay.max(BigDecimal.ZERO));
}
private String genOrderNo() {
return "OD" + UUID.randomUUID().toString().replace("-", "").substring(0, 18);
}
}
6.10 Controller(只接 DTO,只吐 VO)
控制器这一层就是"边界门禁"。
它不应该知道数据库结构,只应该处理接口语义。
java
package com.example.order.controller;
import com.example.common.PageResult;
import com.example.order.dto.command.PlaceOrderCommandDTO;
import com.example.order.dto.query.OrderPageQueryDTO;
import com.example.order.service.OrderService;
import com.example.order.vo.OrderListVO;
import com.example.order.vo.OrderPreviewVO;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping("/preview")
public OrderPreviewVO preview(@RequestBody @Valid PlaceOrderCommandDTO dto) {
return orderService.preview(dto);
}
@PostMapping
public Long place(@RequestBody @Valid PlaceOrderCommandDTO dto) {
return orderService.placeOrder(dto);
}
@GetMapping
public PageResult<OrderListVO> page(OrderPageQueryDTO query) {
return orderService.page(query);
}
}
7. 这一套分层真正带来的收益
你把这套结构跑一遍后,会明显感受到:
7.1 输入变化不会污染存储
前端新增字段、校验变化
只影响 DTO。
7.2 业务变化有明确归宿
折扣/计费策略变化
只动 BO + Service。
7.3 展示变化不会倒灌数据库
新增状态文案、展示字段、脱敏
只动 VO + Converter。
7.4 复用成本降低
"预览"和"下单"共享一套规则计算
不会出现两份逻辑版本漂移。
8. 可直接写进团队规范的"硬规则"
- Controller:只接 DTO,只返回 VO
- Service:复杂规则必须引 BO
- Mapper/Repository:只处理 Entity/DO
- 禁止:Entity 直接对外返回
- 禁止:一个 DTO 贯穿所有场景
- 必须:对象转换集中在 Converter/MapStruct
9. 结语
对象分层不是"OO 术语游戏",
而是帮助你在真实工程里做到:
变化来了不慌,规则复杂也不乱。
你可以先从这套最小正确实践开始:
Command/Query DTO + BO(复杂时) + Entity + VO + Converter
然后随着业务复杂度升级,再逐步扩展到更完整的架构规范。