
后端各层是否都要有数据 Bean这个问题我见过太多次在代码评审会上被提出来也见过不少团队因为没想清楚把一个对象从头传到尾最后改需求时集体头大。也有团队反向操作不管什么场景都强制搞一套 Command、DTO、Entity、PO 四件套结果一个简单的列表查询要写四个类加三个转换方法开发效率肉眼可见地往下掉。先把结论放在前面不是所有层都需要自己的数据 Bean但每一层都应该有明确的“数据边界意识”。该拆的对象必须拆清楚能简化的场景也完全可以直接复用或合并。这篇文章就围绕 Command、DTO、Entity、PO 这四个最常见的对象模型把它们的职责边界、适用场景、转换策略和简化时机一次讲透。适合正在做分层架构设计、被各种对象命名搞得头疼的后端开发也适合刚接触 DDD 或想梳理清楚 Service 层职责的团队参考。1. 四个数据 Bean 的职责边界1.1 从一次下单请求看对象的流转用一个最典型的电商下单场景来拆解。假设用户从前端提交一个创建订单的请求包含用户 ID、商品 ID 列表、收货地址备注等信息这个请求进入系统后至少要经历几个阶段接口校验入参、业务逻辑处理、数据落库、返回响应结果。如果不做任何区分这整个过程可能只有一个 Order 对象在流转。但仔细想一下这几个阶段的诉求完全不同。接口层关心的是“用户这次想干什么”所以它需要一个干净的入站对象来描述用户意图业务层关心的是“订单应该怎么创建、需要哪些不变量校验”它需要一个能承载业务规则的对象持久化层关心的是“怎么把订单数据映射到数据库表结构”它需要一个和表字段一一对应的对象。四个人想要的东西不一样硬塞进一个类里最后只会出现一种情况这个类同时被 Controller、Service、Repository 三个层引用任何一个层的改动都可能导致其他层跟着遭殃。我见过一个真实项目Order 类上有几十个字段一半是给前端展示用的冗余字段一半是数据库表字段还有几个是业务计算过程中的临时状态位。加了字段没人敢随便删因为不知道哪个调用方正在依赖它。后来团队花了整整一个迭代把对象拆开才把代码从“改一处崩三处”的泥潭里拉出来。1.2 Command描述“用户想做什么”的入站意图Command 对象的核心特征是它代表一个动作的入参是请求侧的最小完整语义单元。比如“创建订单”是一个动作那 CreateOrderCommand 里就应该包含创建订单所需的全部输入信息用户 ID、商品条目、备注等。为什么不能直接用 MapString, Object 或者一堆零散的参数两个原因。第一Map 完全无法表达结构和约束校验逻辑只能靠手工写IDE 重构和编译期检查全部失效第二零散参数当数量超过三四个时可读性就急剧下降而且新增一个参数会牵连所有调用方的方法签名。Command 对象最适合放在接口层或者专门的 Application 层做参数收集和基础校验。它不该包含任何业务规则也不该有数据库相关字段因为它只是“意图”的载体。一个合格 Command 的命名应该直接表达动作比如 CreateOrderCommand、UpdateUserProfileCommand、CancelOrderCommand让人一眼就知道这个对象对应哪个业务操作。1.3 Entity唯一有“资格”承载业务逻辑的对象Entity 是整个分层的核心也是最容易被误解的一个角色。很多人把 Entity 理解成“数据库表映射类”这是个经典误区。Entity 在 DDD 语境下是领域模型它的职责是承载业务规则和不变量而不是简单搬运数据库字段。举个例子订单创建时有一个核心业务规则订单总金额必须大于 0且商品数量不能超库存。这个校验应该放在哪不是放在 Controller 里也不是放在 Service 的 if-else 里而是放在 Order 这个 Entity 的领域方法里比如 order.place()、order.addItem()。这样一来任何想创建一个异常订单的路径都会被挡在业务规则之外而不是靠某个 Service 方法“恰好记得”做校验。Entity 的生命周期可以跨越多个层但它有一个铁律不能直接暴露给接口层。接口层如果拿到 Entity前端就能直接看到内部状态甚至被反序列化注入数据这等于把领域模型裸露在外部。所以 Entity 如果要返回给前端必须转换成 DTO。1.4 PO纯粹为数据库表结构服务的对象POPersistent Object是三层架构时代的经典概念在 MyBatis 中对应一个个 ResultMap 映射类在 JPA 中对应带 Entity 注解的表映射类。它的字段设计完全跟着数据库表走一张表一个 PO字段名和表字段名一一对应不承载业务逻辑。PO 和 Entity 最大的区别在于视角。Entity 是从业务视角建模的PO 是从存储视角建模的。同一个“订单”从业务上看有订单号、下单用户、商品条目、优惠明细、应付金额等但从表结构上看订单主表、订单条目表、订单优惠表可能是分成三张表存储的。如果强行让 Entity 直接持有这三张表的映射关系领域模型会被存储细节污染反过来 PO 也会因为承载了业务规则变得难以维护。所以 PO 的唯一职责就是做 ORM 映射它的字段增删应当跟随表结构变更而不是跟随业务逻辑变更。1.5 DTO跨层传输与对外响应的“格式化信封”DTOData Transfer Object是个相对务实的角色它的核心定位是“传输”。在经典场景里Controller 层拿到 Service 层返回的数据后需要组装成前端想要的 JSON 结构这个过程如果用 Entity 直接序列化大概率会暴露多余字段也容易因为字段命名和前端约定不一致而出问题。DTO 的设计有两种来源。一种是从 Entity 转换而来比如 OrderDTO 是 Order 的对外投影只包含需要展示的字段另一种是纯查询结果聚合比如一个报表查询返回的 OrderSummaryDTO它可能聚合了订单表、用户表、商品表多个数据源这种 DTO 跟任何 Entity 都没有直接关系。一个容易忽略的点是DTO 也分为入站和出站。入站 DTO 用于接收请求体出站 DTO 用于组装响应体。如果接口比较复杂入站可以直接用一个 Command 对象出站用一个 ResponseDTO没必要再额外引入一层入站 DTO。具体怎么取舍后面实操部分会展开讲。2. 为什么“一个 Bean 走天下”会出问题2.1 职责不清导致的字段爆炸很多人觉得“我们项目简单一个 User 对象从头传到尾也没出事”。这种想法在项目早期确实成立因为表少、字段少、调用链短。但一旦业务开始增长问题就会以最快的速度暴露出来。还是拿用户来举例。用户表有 20 个字段如果数据库映射对象、业务对象、接口返回对象都是同一个 User那这个类就成了所有信息的大杂烩。数据库查询时你把密码哈希字段放进来了列表展示时你把用户积分和钱包余额放进来了更新接口时调用方传了一个带 null 的对象你根本分不清是“某个字段没传”还是“某个字段的值应该被清空”。这种模糊性在数据处理上是致命的。有个非常经典的线上事故案例某团队用一个大对象做更新操作前端只传了三个字段框架自动把其余字段全部置空再执行更新结果把整行数据的业务字段清空了。如果按职责拆开更新场景用一个专门的 UpdateUserCommand只包含允许修改的字段就永远不会踩这种坑。2.2 请求校验与业务规则无处安放一个对象被多个层复用的另一个后果是校验逻辑不知道该放在哪。Controller 层收到请求后做了参数非空校验Service 层又做了一遍业务规则校验Repository 层因为怕脏数据又加了一层防御性判断。结果就是同一段校验逻辑散落得到处都是改了 A 处忘了 B 处状态不一致的问题早晚出现。标准的分层做法是接口层只做基础格式校验比如字段类型、长度、必填Command 对象进入 Service 层后由领域对象在执行业务方法时做业务规则校验。这两类校验的层次完全不同一个太弱的请求根本不该进入业务层而业务不变量必须固化在 Entity 的方法里。2.3 跨层耦合导致的“改表必改接口”如果 Controller、Service、Repository 共用同一个 PO 对象数据库表结构就变成了对外接口的隐式契约。DBA 想加一个普通字段前端联调时发现返回的 JSON 多了一个无关字段数据库拆分表了所有接口响应跟着变连修改一个字段的注释长度都可能影响前端展示逻辑。这种耦合的直接后果是数据库结构无法独立演进。而如果中间隔了一层 DTO数据库表结构调整时只要 Service 层的转换逻辑兜得住对外响应完全不需要变化。团队协作的边界也会清晰很多前端只依赖 DTO 结构后端可以自由调整 PO 和 Entity互相不打扰。2.4 场景决定架构什么情况下“一个 Bean”是够的当然上面说的这些都是针对复杂业务系统。如果你的项目是个简单的内部管理后台只做几张表的增删改查字段不超过十个用户量也有限那确实没必要为了分层而分层。这时候用 PO 对象同时充当 DTOController 直接操作 Service 返回的 PO效率才是最重要的。但哪怕在这种简单场景下我也建议至少保留一个 Command 或 Request 对象因为请求入参和持久化对象天然存在差异。最轻量的折中方案是入站用一个请求对象出站直接用映射对象。这样既控制了请求校验又避免了复杂化是一个很务实的中间态。3. 边界意识不同场景下 Bean 的取舍策略3.1 Service 层的查询场景Command 与 DTO 的轻量组合最容易过度设计的就是查询类接口。比如后端提供一个“订单列表查询”接口查询条件可能有关键词、订单状态、时间范围、分页信息。这些查询条件能复用 Entity 或 PO 吗完全不能因为它们根本不是同一个事物。这种场景我推荐用一组轻量组合查询入口使用一个 Query 对象或 QueryCommand比如 OrderPageQuery包含查询条件和分页信息返回结果用一个 PageDTO 。中间不经过 Entity也不需要 PO 参与因为纯粹是读操作没有业务规则需要承载。这样处理后查询条件的变更只影响 Query 对象和 SQL 映射返回结果字段的调整只影响 DTO 结构。即使是几百行的列表查询代码改动范围也能控制在一个层内。3.2 报表聚合场景不需要 Command也不需要 Entity报表类的数据接口是另一个极端。这类接口的输入往往非常简单可能就是一个日期区间加上一个分组维度输出则五花八门可能是趋势折线数据、可能是排行榜数组、可能是多维汇总表格。如果用标准的四件套去套你会发现自己造了一堆没有任何业务含义的中间对象。我的建议是报表场景直接用“查询条件对象 结果 DTO”的组合省掉 Command 和 Entity。查询条件对象是入参结果 DTO 是经过 SQL 聚合后直接组装的出参中间完全没有领域模型参与。唯一的注意点是结果 DTO 的字段命名要清晰最好和前端约定保持同一份文档避免反复沟通成本。3.3 读写分离与 CQRS查询侧允许“降配”很多人对 CQRS 有些误解觉得是大规模微服务架构才用的东西。其实 CQRS 的核心思想非常简单把写操作和读操作的数据模型分开。写操作关注命令和结果状态读操作关注查询条件和投影结果。落到数据 Bean 上CQRS 对分层最直接的启发是读模型不需要 Entity。查询场景完全可以用轻量 DTO 直接映射 SQL 查询结果不需要去构造一个领域对象来“假装”承载逻辑——因为查询路径根本没有逻辑要承载。就算不想正式引入 CQRS也可以借鉴这个思路在 Service 层把“写方法”和“读方法”分开编排。写方法接收 Command、返回 DTO内部使用 Entity 和 PO读方法接收 Query、返回查询专用 DTO直接走一个轻量查询通道。两边的对象互不干扰代码的可读性和可维护性都会有明显提升。3.4 简单 CRUD 场景的实用简化方案给新手团队一个实操参考。如果你们的模块真的只是简单 CRUD没有复杂的业务规则我推荐一套保守但高效的最小分层方案入站参数用一个 Command 或 Request 对象集中在 Controller 层接收和校验。存储映射用一个 PO 或实体类直接对应表结构用于 ORM 读写。业务逻辑在 Service 层处理不单独抽象 Entity而是把少量业务规则写在 Service 方法内部或抽成工具方法。出站响应如果响应结构很简单直接用 PO 序列化也可接受如果前端字段要裁剪或重命名再引入 DTO。这套方案的特点是灵活。当项目规模变大、业务规则复杂起来后你只需要把 Service 里不断膨胀的逻辑下沉到新建的 Entity 中把出站直接序列化替换成 DTO 转换就能平滑过渡到完整的分层结构代码改动不需要推倒重来。4. 初始化与转换从 Command 到 Entity 再从 PO 到 DTO 的实现细节4.1 对象转换的三个层次与工具选型对象之间的转换是分层架构里最繁琐也最容易出 bug 的环节。转换手段从低级到高级有三个层次第一层是手写 get/set。适合字段数量少、转换逻辑特殊的场景代码直观但也最啰嗦。一旦字段超过十个手写转换的代码就会占据大量行数阅读疲劳感很重。第二层是 BeanUtils 等反射工具。Apache Commons BeanUtils 和 Spring 的 BeanUtils 都能复制同名属性缺点是性能一般、字段名不一致时必须手动处理而且 BeanUtils 是浅拷贝嵌套对象复制时会有隐患。如果需要快速做 demo用一下无妨但核心逻辑链路不建议依赖它。第三层是编译期映射框架比如 MapStruct。这是我目前在小规模团队里最推荐的方式。MapStruct 在编译时生成转换代码没有运行时反射开销字段映射错误能直接编译报错还支持自定义转换方法。用起来也很简单定义一个 Mapper 接口框架自动实现实现类。注意用 MapStruct 时字段名如果不一致要在接口中用 Mapping 注解显式指定。编译期会给你明确的报错提示比运行期跑出 NPE 再排查要舒服太多。4.2 一个完整下单链路的代码示例下面用一段完整的代码演示从 Controller 到 Repository 的对象流转。这个例子是下单流程的简化版但涵盖了 Command、Entity、PO、DTO 四种对象的典型用法。// 1. Controller 层接收 Command返回 DTO PostMapping(/orders) public OrderDTO create(RequestBody Valid CreateOrderCommand cmd) { return orderService.create(cmd); } // 2. Service 层Command 转 Entity执行业务再转 DTO 返回 Transactional public OrderDTO create(CreateOrderCommand cmd) { Order order Order.create(cmd.getUserId(), cmd.getItems()); orderService.save(order); return OrderDTO.from(order); } // 3. Domain 层Entity 承载领域规则 public class Order { private OrderId id; private UserId userId; private ListOrderItem items; private Money totalAmount; private OrderStatus status; public static Order create(UserId userId, ListOrderItem items) { if (items null || items.isEmpty()) { throw new IllegalArgumentException(订单商品不能为空); } Money total items.stream() .map(OrderItem::getAmount) .reduce(Money::add) .orElseThrow(() - new IllegalStateException(订单总额计算失败)); if (total.isLessThanOrEqualToZero()) { throw new IllegalArgumentException(订单总额必须大于零); } Order order new Order(); order.userId userId; order.items items; order.totalAmount total; order.status OrderStatus.PENDING_PAYMENT; return order; } } // 4. Repository 层Entity 转 PO存储 public class OrderRepositoryImpl implements OrderRepository { public void save(Order order) { OrderPO po OrderMapper.INSTANCE.toPO(order); orderDao.insert(po); } } // 5. MapStruct Mapper编译期生成转换代码 Mapper public interface OrderMapper { OrderMapper INSTANCE Mappers.getMapper(OrderMapper.class); OrderPO toPO(Order order); }注意看每一层之间传递的到底是什么Controller 接触 Command 和 DTOService 接触 Command、Entity 和 DTORepository 接触 Entity 和 PO。没有哪一层直接暴露了不属于它的对象类型每个对象的生命周期都被严格限定在对应的职责范围内。4.3 Command 设计时最关键的校验参数设计 Command 类时有三个参数维度值得认真考虑第一个是入参的必填与边界。用 JSR-303 注解NotNull、NotBlank、Min、Max做基础约束这是接口的第一道防线。但要注意这类注解只能做“字段是不是存在、格式对不对”的校验不能做跨字段的业务一致性校验。第二个是是否包含冗余字段。Command 里只放本次动作需要的字段不要把用户表的所有字段都塞进去。例如“更新手机号”这个动作只需要 userId 和 newPhoneNumber而不需要昵称、头像、性别。第三个是字段类型是否清晰。不要为了省事把两个关联的对象合并成一个 Map 或 String那会让校验和转换代码变成一团乱麻。宁可多写一个嵌套内部类也要让结构关系明确。4.4 DTO 转换时的字段裁剪与组装策略出站 DTO 最常踩的坑是直接用工具类复制所有字段然后发现前端拿到了不该看到的内部字段。比如订单 PO 里有个“支付回调状态”字段本来只有后端系统内部关心结果被复制到 DTO 里返回前端了。这不仅是安全问题也容易误导前端做出错误的交互。正确的做法是先确定 DTO 的“展示契约”。定义一个 OrderDTO 时只包含前端需要展示的字段然后用 MapStruct 或手写 getter 显式赋值。如果某些字段需要聚合多个数据源在 DTO 的 from 方法或 Mapper 接口中自行装配。实操提示如果你用 Jackson 做 JSON 序列化建议给 DTO 类统一加上 JsonInclude(JsonInclude.Include.NON_NULL) 注解避免 null 字段出现在响应里前端拿到的 JSON 更干净排错时也更好定位。4.5 转换代码放在哪个位置更合理对象转换代码的位置是个容易引发争论的问题。我的建议是转换属于基础设施不属于业务逻辑所以不要写在 Service 方法中间的 if-else 里。首选方案是放在 Mapper 接口中由 MapStruct 统一管理比如 OrderMapper.toDTO(entity)、OrderMapper.toPO(entity)。第二种方案是放在 DTO 或 Entity 的静态工厂方法中比如 OrderDTO.from(order)适合字段少、转换逻辑简单的场景。第三种方案是放在 Repository 实现类里只做 Entity 与 PO 的互转。凡是出现三处以上重复转换逻辑的都建议抽到 Mapper 层避免散落各处改起来头痛。5. 命名规范与分层边界避免“四不像 Bean”出现5.1 各类 Bean 的命名风格建议命名是分层边界最容易露馅的地方。一旦命名混乱代码就失去了自解释性。这里给出我习惯的命名风格入站动作对象动词名词Command如 CreateOrderCommand、UpdateUserCommand。入站查询对象名词Query 或 Condition如 OrderPageQuery、ProductSearchCondition。持久化映射对象表名前缀PO如表 order 对应 OrderPO表 user 对应 UserPO。领域实体直接用领域名词如 Order、User、Product不附加任何后缀。出站响应对象业务名DTO 或 ResponseDTO如 OrderDTO、OrderItemDTO、UserProfileDTO。报表聚合对象业务名SummaryDTO 或 StatisticsDTO如 SalesSummaryDTO。命名一旦定下来整个团队的代码风格就自然形成一道“物理约束”看到后缀就知道这个对象属于哪个层不该在哪个层使用。5.2 跨层边界上的典型坏味道我这里列几个代码评审中经常遇见的“坏味道”大家可以自己对号入座Controller 方法直接接收 PO 作为参数等于把数据库表结构暴露给接口层破坏力极强。Service 方法返回 Entity 而不是 DTO前端能看到领域内部结构且 Service 层一旦改动领域方法接口响应跟着变。Entity 类里出现表字段注解如 TableField、Column说明你把 Entity 和 PO 混用了领域模型被持久化框架绑架。Command 类里出现多个动作混合字段比如一个类同时支撑创建和更新两个动作动作的字段约束互相干扰校验逻辑会变得极其混乱。PO 类里写了业务计算逻辑存储对象不该有行为有行为的对象很快会变成维护黑洞。这些坏味道不一定会立刻触发 bug但会在后续的每一次需求变更中慢慢放大直到某个字段的改动牵扯出一大段未知报错。5.3 谁负责定义对象之间的转换契约对象转换契约应该由谁定义答案是入边和出边的边界层。Controller 是 HTTP 入站和出站的边界定义 DTO 结构Application 或 Service 层是应用服务模型的边界定义 Command 与 Entity 的转换Repository 是持久化的边界定义 Entity 与 PO 的转换。每一层负责自己的那一段不要跨层定义。例如前端需要一个“下单成功后返回订单号、应付金额、预计送达时间”的响应这个 DTO 结构应该在接口文档或 Controller 层定义清楚Service 层只需要按照契约返回即可。如果 Service 层自己重构了返回对象但 DTO 没跟上接口契约就悄悄变化了这种问题往往要等前端联调时才会暴露成本相当高。5.4 团队规范落地的三个抓手让每个成员都遵守分层规范光靠口头要求是不够的。我有三个比较落地的办法第一架构守护工具。用 ArchUnit 在测试里写规则比如“Controller 不能依赖 PO”、“Repository 层不能返回 Entity 给 Service 之外的地方”这些规则会作为单测跑在 CI 里违反了直接红掉。第二代码模板标准化。在 IDE 里配置好 class 模板新建 Controller、Command、Entity、PO、DTO 时自动生成对应后缀与注释结构减少命名随意的概率。第三评审 checklist 化。代码评审时可以拿着“是否有 PO 泄漏出 Repository”“是否有无业务含义的万能对象”这类清单逐项过比泛泛而谈“注意分层”有效得多。6. 常见问题与避坑经验实录6.1 为什么 PO 和 Entity 字段对不上时容易出 bug最常见的原因是数据库表拆分了但领域模型还保持原来的结构。比如订单表拆成了订单主表和订单扩展表PO 就变成了两个但 Entity 在领域概念上仍然是一个完整的 Order。如果转换逻辑没写清楚最容易出现的 bug 是保存订单时只写了主表扩展表漏了或者查询订单时只查了主表数据扩展字段全是 null。排查这种问题有一个比较实用的办法在 Repository 实现类的保存方法里做一次“结构完整性断言”比如保存前检查扩展表 PO 是否有主键关联查询后检查关键字段是否为空。虽然不能杜绝所有问题但能把大部分低级遗漏挡在测试阶段。6.2 Command 与 DTO 到底有什么区别能不能合并这是团队里争论频率最高的一对概念。简单说Command 表达的是“动作”DTO 表达的是“数据”。CreateOrderCommand 里的字段是“创建订单”这个动作的入参集合OrderDTO 里的字段是“订单”这个物体的对外数据结构。两者的视角完全不同。在实际项目里如果创建动作的入参和出站结构恰好高度相似比如内部管理系统的字典数据维护Command 和 DTO 可以合并成一个对象吗我的答案是可以但要有前提。前提是这个接口永远不会分叉且字段数量很少。一旦字段超过十个或者前后端展示结构和入参结构开始出现差异立刻拆开。合并带来的收益是少写一两个类拆开带来的收益是长期内不会互相牵连。6.3 Entity 能不能直接当 Mapper 里的 PO 用这个问题取决于持久化框架。在 MyBatis 里Mapper 映射的是 PO如果你把 Entity 直接写进 resultMap等于让领域模型背负了表结构映射的职责。短期看省事长期看领域模型的演进会被表结构绑架比如表结构升级加了一个冗余的统计字段Entity 就得跟着加一个没有任何业务含义的属性。如果你用的是 JPA/Hibernate情况稍微特殊一些因为 Entity 本身就是持久化实体它天然承担了表映射和领域模型双重职责。这是框架的设计取向不能算错。但如果团队采用的是 DDD 风格我更推荐用 JPA 但把“领域实体”和“存储映射”通过接口隔离或者在映射层做一个轻量适配避免贫血模型和映射模型混成一片。6.4 转换性能问题MapStruct 和手写的取舍有同学担心引入 MapStruct 会增加维护成本或者性能损耗。实际上 MapStruct 在编译期生成了最朴素的 getter/setter 调用代码运行效率和手写完全一致。唯一的成本是编译时间略微增加以及注解处理器的配置需要在 pom 或 gradle 里加好。但有一个性能问题值得注意如果列表数据量很大比如一次性返回几千条用户记录每次都对集合元素做 DTO 转换即使是一次性几十条数据整体耗时也可能变得明显。这种情况下可以在 SQL 层面直接查询出 DTO 需要的字段使用 resultType 映射或 JPA 投影省去转换开销。这也解释了为什么很多查询类接口的“返回 DTO”其实是直接从查询映射层生成的而根本没有经过 Entity 转换。6.5 常见问题速查表问题现象可能原因推荐处理方式Controller 方法参数是 PO 类PO 泄漏到接口层定义 Command 类接收Controller 内转 CommandService 返回 Entity前端拿到额外字段缺少出站 DTO定义 DTO用 MapStruct 显式转换Entity 类上有 MyBatis 注解Entity 与 PO 混用拆分 Entity 和 PO或明确采用 JPA 风格修改数据库表后前端接口报错PO 与 DTO 直接耦合在 DTO 中剥离展示字段隔离表结构变化一个 Command 字段几十个动作意图不够纯拆分 Command按动作维度细化入参一个对象被 5 个层引用全能对象出现按职责拆分每层只依赖自己的边界对象最后说点个人体会分层这件事说到底不是为了追求架构上的“政治正确”而是为了控制变化的传播半径。这个原则聊到最后我特别想说一句每个项目初始阶段都不需要严格全套的 Bean 分层但都需要清晰的边界意识。边界意识在事后补拆分是体力活边界意识缺失事后重构就是大手术。分清哪些场景该上全套对象、哪些场景该大胆合并比拿着一套流程走天下靠谱得多。希望这篇内容能帮你在设计数据 Bean 时少走几段弯路也欢迎在评论区聊聊你们团队的不同取舍。