ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

一文搞懂DAO/DO/DTO/VO/BO/POJO:分层架构数据对象全解析

一文搞懂DAO/DO/DTO/VO/BO/POJO:分层架构数据对象全解析 1. 这一堆缩写到底在说什么一次讲清楚刚入行那会儿我也有过一段“看文档像看天书”的经历。打开公司的老项目里面既有User、UserDO又有UserDTO、UserVO、UserQuery偶尔还冒出UserBO和UserAO。最崩溃的是你去问带你的老员工每个人给的答案还不太一样有人说“DO就是数据库对象”有人说“DO是领域对象”还有人直接甩一句“别管那么多按分层放就行了”。后来自己踩了不少坑、也重构过几个烂摊子项目才慢慢想明白一件事DAO、DTO、DO、VO、AO、BO、POJO、PO、Entity、Model、View 这些概念本身没有多复杂复杂的是它们在不同框架、不同团队、不同历史阶段里被塞进了不同的含义。你要是不理解背后的演变逻辑光靠死记硬背定义今天记住了明天照样混。这篇文章我把它们串成一个整体来讲不只是挨个给定义而是说清楚每个词到底在解决什么问题为什么会有这么多看起来差不多的概念在真实的分层架构里它们各自站在哪个位置哪些是你项目里真正要用的哪些是历史遗留下来的“马甲”。我尽量用大白话配合代码示例和对比表格保证你看完能直接落地到自己的项目里而不是看完就忘。先给一个总览表把这一堆概念先按“归属层”和“本质”归一下类心里有个地图再往下看概念本质典型归属层核心职责POJO普通Java对象纯数据无框架约束任意层一切简单对象的统称PO / Entity / Model / DO和数据库表结构对应的持久化对象数据访问层承载数据库表的字段映射DAO数据访问对象操作数据库的接口/类数据访问层封装SQL与持久化操作DTO数据传输对象跨层/跨网络传数据接口层与服务层之间按调用方需要组织字段VO视图对象直接给前端展示用表现层/接口层聚合展示所需字段BO业务对象承载业务逻辑处理后的数据业务逻辑层组织业务规则与组合数据AO应用对象面向应用/远程调用场景应用层/RPC层跨系统集成时的数据契约这张表先当索引用下面每一类我都会展开讲包括它们之间的边界在哪里、什么场景必须区分、什么场景可以偷懒以及偷懒会有什么后果。2. POJO、PO、Entity、Model、DO持久化对象家族的“历史演变”先搞清楚最底层的东西因为这一组概念最绕而且绕的原因是历史遗留问题不是逻辑问题。2.1 POJO 是所有“简单对象”的根POJO全称 Plain Old Java Object直译是“朴素的老式Java对象”。这个词是 2000 年左右 Martin Fowler 等人造出来的当时是为了讽刺 EJB 那套重量级企业组件的复杂程度——你们搞那么重我们写一个最简单的class User { private String name; }不行吗所以 POJO 的定义极其宽泛只要你的类没有继承某个框架的基类、没有实现框架的接口、没有打上框架的注解就是一个 POJO。就这么简单。它跟架构、分层、业务完全不挂钩纯粹描述一种“干净”的代码状态。但问题来了现实项目中我们几乎不可能不用框架。你用了 MyBatisUserDO就要考虑和表字段的映射你用了 SpringController 的入参可能就要配合注解做校验你用了 JPAEntity就必须加Entity注解。所以现在说“这是一个 POJO”更多是在强调“这个对象的本质是数据载体不掺框架逻辑”而不是说它真的什么注解都没有。提示面试里如果问“POJO 和 JavaBean 有什么区别”标准答案是——POJO 强调“纯净”JavaBean 强调“有规范”即属性私有、提供 getter/setter、可序列化。但实际工作中没人较真这个你只要知道 POJO 是个统称就够了。2.2 PO 是“历史课本”里的概念现在很少单独说PO全称 Persistence Object持久化对象。它和数据库表结构一一对应一个字段映射一个列。这个概念在早年 Hibernate 盛行的时代非常流行后来 MyBatis 慢慢成为国内主流之后大家开始叫 DOData Object或者直接叫 EntityPO 这个词就逐渐被替代了但在很多老项目的注释和设计文档里还能看到。PO 的典型特征有三个字段和表结构完全对齐数据库表里有什么列PO 里就有什么字段基本不包含业务逻辑它就是数据的“搬运工”生命周期和数据库会话相关从查询出来到被修改回写它一直描述的是数据库里那一条记录。2.3 Entity 是“框架语境”里的叫法JPA 时代的主力Entity 这个叫法主要跟着 JPAJava Persistence API走。你在代码里写Entity Table(name user) public class UserEntity { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name user_name) private String userName; }Entity注解告诉 JPA 框架这个类代表数据库里的一张表你可以通过它来做 ORM 映射。所以 Entity 和 PO 本质上是一码事只是“Entity”是框架官方的叫法“PO”是民间习惯的叫法。现在很多新项目尤其是用 Spring Data JPA 的团队就直接叫XxxEntity而用 MyBatis 的团队更喜欢叫XxxDO或XxxPO其实都是一回事。2.4 DO 在 MyBatis 生态里的地位事实标准国内 MyBatis 用得太多了所以 DOData Object基本成了“数据库映射对象”的默认叫法。阿里开发手册里也推荐数据对象用XxxDO命名比如UserDO、OrderDO。DO 和 PO/Entity 没有任何本质区别就是名字不同。不过有一点要注意如果你是做领域驱动设计DDD的DO 有时候会指 Domain Object领域对象含义完全变了。这时候一定要看上下文别拿着 MyBatis 的理解去套 DDD 项目会出大乱子。2.5 Model 是“最随意”的叫法能不用尽量别用Model模型这个词问题最大因为它太泛了。有人拿 Model 指数据库实体有人拿 Model 指页面展示模型还有人直接拿 Model 当“什么都往里放”的工具类。我见过最典型的烂代码是这样public class UserModel { private Long id; private String username; private String password; private String createTime; private String blogTitle; private String blogContent; // 混合了用户和博客的字段因为“先这么放着后面再说” }这种类在项目里滚半年之后谁都不敢动它因为不知道它到底代表什么、被哪些地方依赖。所以我的建议是项目命名规范里明确废弃Model后缀强制用Entity、DO、DTO、VO这类语义明确的后缀。这不是教条这是为了让你三个月后回来看代码时不用靠猜。2.6 这一组概念在代码里的实际对比我写了一个简单的用户查询例子展示同样一张user表在不同框架下的映射类长什么样// 传统 PO 风格Hibernate 时代 public class UserPO implements Serializable { private Long id; private String userName; private String password; // getter/setter 省略 } // MyBatis 团队常用的 DO 风格 public class UserDO { private Long id; private String userName; private String password; // getter/setter 省略 } // JPA 风格 Entity Table(name user) public class UserEntity { Id private Long id; Column(name user_name) private String userName; private String password; // getter/setter 省略 }这三个类映射到的是同一张表字段一模一样区别只在于名字和注解。选哪个完全取决于团队技术栈没有对错。3. DTO、VO、BO、AO按“场景”切分的数据视图如果说上面那组概念是“数据库视角”的那这一组就是“业务视角”的。它们的核心思想是同一份数据在不同场景下需要不同的“长相”。3.1 DTO跨层传输的“快递箱”DTO全称 Data Transfer Object数据传输对象。它的核心使用场景是方法调用、接口调用、网络传输时把数据打包传递。举一个最常见的例子。你的数据库用户表有 20 个字段但是给前端的接口只需要返回 5 个字段而且这 5 个字段里有两个是后端计算出来的、表里根本没有。这时候你直接把 DO 返回出去就会泄露多余字段而且扩展不灵活。正确的做法是定义一个 DTOpublic class UserDTO { private Long id; private String username; private Integer age; private String address; private String vipLevel; // 计算出来的数据库表里没有 // getter/setter 省略 }DTO 的核心价值在于“按需包装”。它不关心数据库表长什么样只关心调用方需要什么数据。这里要特别强调一个点DTO 是一个过程概念不是一个结果概念。一个对象在这个接口里是 DTO在另一个场景里可能就变成 VO 了。很多人搞混 DTO 和 VO就是因为他们忽略了这个“场景”属性。3.2 VO专门给前端看的“护肤品”VO全称 View Object视图对象。它和 DTO 非常像区别在于使用场景更聚焦专门用于 Controller 层返回给前端的数据组装。举个例子前端要展示一个用户主页需要用户名、头像、粉丝数、文章数、最近登录时间这些数据分散在 user 表、user_profile 表、article 表的统计结果里。你在 Service 层把数据全部查出来然后在 Controller 层组装成一个UserProfileVO返回public class UserProfileVO { private Long userId; private String nickname; private String avatarUrl; private Integer followerCount; private Integer articleCount; private String lastLoginTime; }这个 VO 存在的意义是前端不需要关心数据从哪里来后端也不希望把内部结构暴露给前端。你改了数据库表结构只要 VO 字段不变前端完全感知不到。那 DTO 和 VO 到底什么区别业界一直有两种说法第一种最主流DTO 是传输用VO 是展示用。DTO 可以穿过多层VO 只在表现层出现第二种当一个对象只用于视图展示、不需要再往下游传递时它就是 VO当一层收到数据后需要继续传给下一层它就是 DTO。我觉得你不用死记哪个对你只需要记住一个判断方法如果这个对象下一步要被别的系统/服务消费它更偏 DTO如果它只是最终渲染给前端它更偏 VO。很多项目里直接把查询结果对象叫XxxResultVO也不会有人骂你因为边界在某些场景下本来就是模糊的。3.3 BO业务逻辑运算的“工作台”BO全称 Business Object业务对象。它是三者中“业务味道”最重的一个用来承载业务逻辑加工后的数据。举例下单接口里你查出了用户信息UserDO、商品信息ProductDO、库存信息StockDO然后要计算优惠、算运费、算总价判断库存是否充足。这时候你不能把所有 DO 直接丢给一个方法而是应该组装成一个OrderBO把计算需要的字段都放进去public class OrderBO { private Long userId; private ListOrderItemBO items; private BigDecimal totalPrice; private BigDecimal discountPrice; private BigDecimal freightPrice; private BigDecimal payPrice; private boolean stockEnough; // getter/setter 省略 }BO 可以理解成一个“业务工作台”上面摆满了计算所需的原料也放着计算过程产生的中间结果。Service 层的方法参数和返回值如果是 BO代码的可读性和可测试性会明显提升。不过说实话BO 现在在国内项目里的地位有点尴尬。一方面阿里手册里没强制用它另一方面很多团队觉得“Service 方法直接返回 DTO 不就行了为什么还要多一层 BO”。我的看法是简单的 CRUD 项目确实不需要 BO但业务流程复杂、一个操作要编排多个数据源、算一堆中间值的项目BO 能帮你把逻辑拆得清爽很多。要不要引入取决于你的业务复杂度不要为了“规范”而规范。3.4 AO应用对象的“特殊场景马甲”AO全称 Application Object应用对象。这个概念出现频率最低主要出现在两类场景微服务/RPC 调用两个系统之间通信定义一个独立的UserAO作为服务间数据契约避免服务内部 DO/DTO 的字段变化直接影响外部 API。应用层入参某些团队把 Controller 接收前端请求参数的对象命名为XxxAO代表“应用层输入对象”。这两个用法都合理但要注意AO 不是一个通用概念不同团队的含义可能差别很大。如果你在简历里写了“用过 AO”面试官追问起来你自己得能说清楚你的 AO 到底用在哪个场景。我个人的建议是除非项目里已有约定俗成的用法否则新项目不推荐主动引入 AO 概念。因为同类工作用 DTO 和 VO 已经能覆盖多加一个概念等于多加一份沟通成本。3.5 一个商城下单场景把 DTO/VO/BO 串起来我画一个典型的下单链路你就明白这三个对象怎么配合了环节对象类型内容说明前端提交订单OrderCreateDTO商品ID、数量、收货地址ID入参对象只传业务所需Service 层处理OrderBO用户、商品、库存、优惠计算结果业务计算的核心工作台查询用户信息UserDO数据库记录直接从底层查询不加工查询商品信息ProductDO数据库记录直接从底层查询不加工计算价格OrderBO原价、优惠、运费、实付BO 内部完成金额计算返回前端OrderDetailVO订单号、金额、状态、预计送达时间只暴露前端需要的字段这个链路里DO 负责“取数”BO 负责“算账”DTO 负责“收参”VO 负责“展示”。每层各司其职改数据库不影响接口改接口不影响数据库。4. DAO数据访问的“中间人”和对象不是一回事前面讲的全是“数据对象”但 DAO 不一样它是一个行为接口。DAO 全称 Data Access Object数据访问对象负责定义操作数据库的方法比如查询、插入、更新、删除。有人会把 DAO 和 Repository 放一起比较业界也经常争论。简单说DAO更贴近 SQL / 表操作把数据库访问封装成方法Repository仓库更贴近领域模型把聚合根的持久化逻辑封装起来属于 DDD 范畴的概念。在实际的 MyBatis 项目中DAO 通常就是 Mapper 接口Mapper public interface UserDAO { UserDO selectById(Param(id) Long id); ListUserDO selectList(UserQuery query); int insert(UserDO userDO); int updateById(UserDO userDO); int deleteById(Param(id) Long id); }DAO 层的核心价值是把 SQL 和业务逻辑隔开。Service 层只需要调用userDAO.selectById(1L)不用关心这条 SQL 是 MySQL 还是 Oracle 的方言也不用关心表结构怎么变化。这是一个非常关键的边界因为一旦业务层直接写 SQL 或者依赖 SqlSessionTemplate后期维护就是一场灾难。注意很多老项目里没有 DAO只有 Service 里直接拼 SQL。短期看着省事一旦表结构变更或者要换数据库改动范围就是“火山爆发式的”。凡是业务代码里直接出现SELECT * FROM的都说明分层已经“透掉了”。DAO 在实践中还有个容易被忽略的细节DAO 的返回值应该用 DO/Entity而不是 Map。虽然 MyBatis 确实支持返回MapString, Object看着很灵活但这样一来类型安全没了字段名拼错要到运行期才报错重构的时候没有任何编译器帮你兜底。能定义 DO 就定义 DO不要图省事。5. “数据类型”的本质这些到底算什么“类型”标题里带了“数据类型”这个词我单独讲一下因为这是新手最容易懵的点DAO、DTO 这些到底是不是“数据类型”严格说它们都是普通 Java 类是引用类型不是基本类型。但在实际开发语境里大家说“数据类型”的时候想表达的其实是两件事一类是数据载体DTO、VO、DO、POJO 这些本质是“装着数据的盒子”它们有属性、有 getter/setter但基本没有行为一类是行为接口DAO 这种本质是“干活的工具”它定义了你对这个数据盒子的操作方式。这个区分非常重要因为很多人把 DAO 当成一个普通类去“new”然后导致代码结构崩坏。DAO 的正确用法是依赖注入让 Spring 帮你管理而不是自己手动创建实例。再看“数据类型”的另一个层面同样一个 User在不同层里应该以不同类型的形态出现。数据库里它UserDO传输时它UserDTO页面上它UserVO。它们字段可能高度重合但语义完全不同。这种“同源异构”的设计是分层架构的核心思想之一。这里我给出一个非常实用的判断方法当你纠结某个字段该放在 DO 还是 DTO 里时问自己三个问题这个字段的源数据在数据库表里存在吗不存在就别放 DO调用方真的需要这个字段吗不需要就别放 DTO这个字段是业务计算后的结果吗是就考虑放 BO 或 VO。按这个思路想大部分边界问题是能自己想明白的。6. 项目实战从零开始设计一套规范的分层数据模型理论讲了半天不落地等于零。下面我以一个“用户管理系统”为例手把手带你把这一整套概念落进代码里。6.1 定义清晰的分层依赖规则首先定规矩这是项目能长期维护的前提分层类名后缀说明Controller 层XxxVO出参、XxxDTO入参接收前端参数、返回前端视图Service 层XxxBO业务逻辑加工和中间结果DAO 层XxxDO数据库表映射DAO 接口XxxDAO数据访问方法定义跨服务调用XxxDTO服务间传输对象依赖规则只有一条上层可以依赖下层下层绝不能依赖上层。Controller 可以调 Service但不能直接碰 DAOService 可以调 DAO也可以跨服务调其他系统的 DTODAO 只处理 DO完全不认识 VO 和 BO。这条规则看着简单实际项目里崩坏的比比皆是。最常见的崩坏方式是Controller 直接把入参 DTO 传给 DAO或者 Service 方法直接返回 VO 给下游系统。短期没出事只是运气好长期必然产生循环依赖或者字段混乱。6.2 用户查询接口的完整代码落盘需求提供一个用户详情接口前端传用户ID返回昵称、头像、等级、粉丝数、最近登录时间。第一步定义 DO映射数据库表public class UserDO { private Long id; private String username; private String password; private String nickname; private String avatarUrl; private Integer userLevel; private String lastLoginTime; // getter/setter 省略 }注意这里password必须出现在 DO 里因为表里有这个字段。但我们绝不能把它直接返回给前端。第二步定义 DAO 接口Mapper public interface UserDAO { UserDO selectById(Param(id) Long id); UserStatisticsDO selectStatisticsByUserId(Param(userId) Long userId); }第三步定义 DTO 作为入参public class UserDetailQueryDTO { NotNull private Long userId; // getter/setter 省略 }第四步定义 VO 作为出参public class UserDetailVO { private Long userId; private String nickname; private String avatarUrl; private Integer userLevel; private Integer followerCount; private String lastLoginTime; }注意password 没有出现在 VO 里。这就是分层带来的安全收益——数据库字段无论如何都在 DO 里完整映射但暴露什么、不暴露什么由每一层的“视图”自己决定。第五步Service 层组装Service public class UserServiceImpl implements UserService { Resource private UserDAO userDAO; public UserDetailVO getUserDetail(UserDetailQueryDTO query) { UserDO user userDAO.selectById(query.getUserId()); UserStatisticsDO stats userDAO.selectStatisticsByUserId(query.getUserId()); UserDetailVO vo new UserDetailVO(); vo.setUserId(user.getId()); vo.setNickname(user.getNickname()); vo.setAvatarUrl(user.getAvatarUrl()); vo.setUserLevel(user.getUserLevel()); vo.setFollowerCount(stats.getFollowerCount()); vo.setLastLoginTime(user.getLastLoginTime()); return vo; } }这个例子看起来简单但已经把 DTO、VO、DO、DAO 四个概念全部串起来了。业务再复杂也只是在这个骨架上增加更多的 DO 组装和计算逻辑结构不会乱。6.3 用 BeanUtils 做对象转换时的坑实际项目里不可能每次组装都手动 getter/setter所以大家会使用 Spring 的BeanUtils.copyProperties或者 MapStruct。用BeanUtils最典型的坑是字段名不一致时静默失败比如 DO 里是userNameVO 里是name复制后 VO 的 name 为 null类型不一致时不报错DO 里Integer ageVO 里String age复制会静默失败性能问题反射调用在超高并发下有不小的开销。我的建议是优先用 MapStruct编译期生成转换代码字段不一致直接编译报错性能和手写 getter/setter 一样。BeanUtils 适合小项目、字段完全对齐的场景不要在大接口上依赖反射拷贝。MapStruct 的示例Mapper public interface UserConverter { UserConverter INSTANCE Mappers.getMapper(UserConverter.class); Mapping(source userName, target name) UserVO toVO(UserDO userDO); }这样即使userName和name不一致也能在编译期指定映射关系不会运行期才发现问题。7. 最容易踩的 6 个坑面试官最爱问项目里最常见这部分我把这些年见过的、问过别人的、自己踩过的问题集中整理一下都是实打实的高频雷区。7.1 DTO 和 VO 真的可以互相替代吗不能直接替代但也不要神化边界。很多团队里 DTO 和 VO 字段完全一样于是有人直接用一个类注释里写着“DTO/VO 通用”。短期确实没毛病但一旦前端要加一个展示字段、后端接口又要同时提供给另一个服务消费这个共用类就开始被两头拉扯最终变成谁都不敢改的“上帝类”。我的建议是即使字段一样也拆成两个类。拆类的成本就是多写几个 getter/setter但换来的是两端的独立演进空间。7.2 Controller 能不能直接返回 Entity/DO这是初学者最常见的问题也是很多老项目的“历史遗留问题”。答案很明确不能。返回 Entity 等于把数据库表结构直接暴露给前端至少有四个问题泄露敏感字段password、salt、内部状态码耦合数据库表结构改表结构影响接口无法装配前端需要的聚合字段如粉丝数、订单数违反最小暴露原则增加被“爬字段”的隐患。7.3 Service 层能不能直接用 Map 返回数据“我这个接口字段太杂不想定义 VO直接返回 Map 行不行”我见过太多这样的代码。临时用 Map 确实很爽但后果是类型安全完全丢失。调用方根本不知道 Map 里有哪些 key也不知道 value 是什么类型只能去翻代码猜。一旦有一个 key 打错了运行期 NPE 或者 ClassCastException 就冒出来了。定义 VO 类编译器能帮你检查IDE 能帮你提示重构时能帮你引用追踪。这几百倍的投资回报率没有任何理由用 Map 代替。7.4 一个类能不能既是 DO 又是 VO如果你的类既直接映射数据库表、又直接返回给前端短期能跑长期必炸。因为数据库表和前端需求的演进节奏完全不一样表结构加字段为了存储VO 加字段为了展示一旦两边需求冲突这个类就不够用了。唯一例外是“极小的内部管理后台”表字段和页面字段完全一致且不会变化。但这种项目太少了我建议还是按规范走。7.5 DTO 里需要做参数校验吗需要而且必须做。DTO 在 Controller 层接收前端参数时就应该完成校验而不是把校验逻辑下沉到 Service 里靠手写 if/else。推荐写法public class UserCreateDTO { NotBlank(message 用户名不能为空) Size(max 32, message 用户名长度不能超过32) private String username; NotBlank(message 密码不能为空) Size(min 6, max 20, message 密码长度需在6-20之间) private String password; }Controller 里加Valid注解即可校验不通过会自动抛出异常根本不会进入 Service 层。这样 Service 层的代码会干净很多。7.6 DO 里能加非表字段吗不能。这是 DO 和 BO 最本质的边界DO 的字段必须和数据库表严格对齐。如果你在 DO 里加一个private String tempValue;这种非表字段将来 MyBatis 自动映射时要么报错、要么忽略后期维护的人根本分不清这个字段是数据库里的还是临时算出来的。非要加非表字段怎么办要么单独定义 BO要么用TableField(exist false)显式标注MyBatis-Plus 的写法让所有人知道这个字段不参与数据库映射。但绝大多数情况下更好的选择是定义 BO。8. 面试实战一套话术帮你把概念题讲出深度这组概念是 Java 后端面试的高频题但大部分候选人只会背定义“DO 是数据库对象DTO 是传输对象VO 是视图对象……”背完就没了。面试官一听就知道你是临时背的。要想讲出深度我建议按下面这个结构来答第一步先给出定位。“这一组概念的核心目的是为了在分层架构中实现数据隔离和对象隔离让每一层只看到自己需要的数据形态。”这句话一出来面试官就知道你不只是背了名字而是理解了设计动机。第二步举实际例子。“比如一个用户详情接口数据库表有20个字段其中包含密码字段。如果我直接用 DO 返回前端就会泄露敏感信息。所以我定义了一个 VO只包含昵称、头像、等级等展示字段。”这个例子能展示你真正用过而且是带着安全意识写的。第三步聊边界和争议。“DTO 和 VO 的边界其实有争议。我的理解是如果一个对象是给服务间传输用的它偏 DTO如果是给前端页面渲染用的它偏 VO。实际项目中同一份数据在不同接口里可能既做 DTO 又做 VO角度不同而已。”这段话展示你有独立思考而不是机械背定义。第四步聊落地方案。“我现在的项目里Controller 只碰 DTO 和 VOService 只碰 BO 和 DTODAO 只碰 DO跨服务调用只允许走 DTO这样每层都有明确的边界。”这是加分项因为大部分候选人只会背定义说不出来自己项目里具体怎么规范。面试官如果追问“Repository 和 DAO 有什么区别”你可以说“DAO 更偏数据库表操作Repository 更偏领域模型聚合根的持久化。传统 CRUD 项目用 DAO 就够了DDD 项目会优先用 Repository。”点到为止即可不用展开太多除非对方明显是 DDD 方向的面试官。9. 一些我踩过坑之后的个人经验写到最后分享几个我自己的体会你可以当成避坑指南来用。第一不要为了“纯正规范”而引入过度设计。一个只有 CRUD 的内部管理系统非要把 BO、AO、VO、DTO 全铺满只会让代码变得繁琐难维护。规范是为了服务项目不是为了好看。项目复杂度低用 DO DTO 两层可能就够了复杂度高再考虑引入 BO。第二把命名规范写进团队的代码规约里而不是靠口头传。我们团队之前因为没有强制规范一个项目里同时出现了User、UserModel、UserDO、UserEntity、TUser新来的同事每次都要问“我要用哪个”。后来统一成数据库映射用 DO、接口出入参用 DTO/VO、业务用 BO并在规约文档里写了例子这个混乱才慢慢消失。第三任何时候都不要用“通用对象”代替“语义对象”。我看到太多人图省事写一个ResultT泛型类就想“通吃所有接口”然后在 Result 里塞一堆 if/else 分支。短期看代码量少了长期看维护成本翻倍。Java 是强类型语言它的类型系统就是用来表达语义的你不用等于白白浪费了语言最大的优势。第四对象转换这事儿越早用 MapStruct 越好。我第一次在项目里手工写了 300 行 getter/setter 之后就再也不想碰 hand-written 转换了。MapStruct 在编译期就能生成转换代码字段对不齐直接编译失败比运行时反射报错好排查一百倍。尤其是大项目重构的时候它的价值会体现得非常明显。第五检验一个项目的分层是否健康有个简单标准如果 Service 层代码平均长度超过 300 行大概率是缺少了 BO 和合理的结构调整如果 Controller 层代码里出现了 SQL 或者拼字符串说明边界已经破了。用这两个标准去扫一眼你现在的项目应该能发现不少问题。这组概念本身不难难的是在真实项目里守住边界。希望这篇整理能帮你少走一些弯路。
返回列表