ARTICLE DETAIL

资讯详情

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

Entity、Model、Domain怎么分?一文理清后端分层架构核心概念

Entity、Model、Domain怎么分?一文理清后端分层架构核心概念 Entity、Model、Domain 这三个词只要做过几天后端开发基本都绕不开。但奇怪的是你去问团队里的不同人十有八九会得到互相矛盾的答案——有人说 entity 就是数据库表映射有人说 model 是数据传输对象还有人把 domain 直接等同于业务逻辑层。我在好几个项目里都见过因为概念不统一导致的架构混乱明明代码分层了可实体类里塞满了接口返回字段领域服务变成了万能工具类最后谁也不敢改核心逻辑。这篇文章就把这三个概念摊开来说清楚从一个真实订单系统的演进过程出发看它们各自的职责边界在哪里。无论你正在做微服务、DDD 重构还是只是想把现有的三层架构理清楚这篇都值得花十分钟读一遍。1. 内容整体设计与思路拆解1.1 这三个词不是同一维度的概念先说一个容易踩的坑很多人把 entity、model、domain 当成三个并列的东西去对比这本身就错了。它们压根不在同一个抽象层面上硬放在一起比较就像问“方向盘、发动机、汽车有什么区别”一样答案永远糊成一团。更准确的理解是entity 是领域层的一种对象形态model 是一个极其宽泛的统称而 domain 是业务问题和业务规则的集合边界。换句话说domain 是你要解决的问题空间entity 是问题空间里某个具有连续性的核心对象model 则是“为了某个目的对现实世界做的抽象”它既可以指领域模型也可以指数据库模型、视图模型、传输模型甚至可以是机器学习模型。所以当你听到“model 和 domain 的区别”这种表述时提问者通常真正想问的是在代码分层里我到底应该把业务逻辑放在哪里数据表映射类的职责边界是什么DTO 和实体到底该不该分开这一层答案才是实际工作中真正需要的东西。1.2 从分层架构看三者的位置我们常见的项目架构无论叫 MVC、三层架构还是整洁架构本质上都在做同一件事把“数据怎么存储”“业务规则是什么”“用户看到什么”这三件完全不同的事拆开。entity、model、domain 这个词各自对应的正好是这三块的典型代表。传统三层架构里数据访问层返回的对象叫 entity它和数据库表几乎是一一对应的业务逻辑层操作的对象有时候也叫 entity但更常被称为 model展示层传给前端的数据对象叫 DTO 或 VO。到了 DDD领域驱动设计的语境里domain 成了核心entity 是领域模型的一部分而 model 反而成了一个模糊的背景板。我在实际项目里的做法是定义一套属于自己的“概念字典”哪怕团队不用完整的 DDD也至少保证大家口中的 entity、model、domain 指向一致。否则写代码的时候各叫各的Review 的时候各说各的设计文档和实现就会慢慢脱节。2. 核心细节解析与实操要点2.1 Entity有标识、有生命周期的对象Entity 最核心的特征是标识Identity。一个对象是不是 entity不看它有几个字段而看它是否具备一条贯穿生命周期的唯一身份。比如一个订单状态从“待支付”变成“已支付”金额从 100 改成 120它作为订单的本质没变订单号还是同一个。这种“变了但还是它”的对象就是典型的 entity。实现层面有几个容易忽略的细节值得说不要用数据库自增 ID 当作唯一的标识。自增 ID 只是存储层的键一旦数据迁移、分库分表或与其他系统合并这个 ID 就可能失效。更好的做法是业务侧生成唯一标识如订单号、UUID保证跨系统也稳定。Entity 内部允许状态变化但变化必须通过方法表达而不是直接暴露字段。比如订单不应该被人从外部直接order.status 2而是应该调用order.pay()、order.cancel()这样业务规则才能内聚在对象里。Entity 集合操作要注意边界。一个订单包含多个订单项订单项是不是独立 entity如果订单项在订单创建后可以被单独增删改它们就是独立 entity如果只能随订单整体创建那它们更像值对象不应该被单独拉出来当 entity。2.2 Model一个被用滥了的词Model 这个词的问题在于它在不同上下文里含义完全不一样。数据库里有数据模型REST API 里有请求/响应模型前端有视图模型算法工程师嘴里有训练模型。你没法问“model 是什么”只能问“这个 model 是给谁用的”。在实际代码里最常见的 model 用法是数据模型也就是和持久层或传输层强相关的数据结构。它通常和 entity 很像但职责更轻——只负责承载数据不承载行为。正因为如此很多人直接把数据库映射类叫 entity把 API 请求参数类叫 model倒也没有大错。但这里有一个真正值得注意的设计原则不要让同一个 model 类到处乱传。如果数据库实体简称 DO直接传给前端作响应前端多出来的字段、后端不想暴露的字段、不同接口之间的字段差异都会把这一层类变成大泥球。更合理的做法是拆成 DO、DTO、VO虽然多写几个类但每个类的职责清晰改起来不牵连一片。实践中我常给团队定的原则是model 是“对象的形状”entity 是“对象的行为 身份”如果某个类只有属性没有方法那它更接近 model而不是 entity。2.3 Domain业务规则与问题空间的边界Domain 这个词来自领域驱动设计指的是你正在解决的业务问题领域。例如电商系统中的订单域、库存域、支付域每个域都有自己的一套通用语言ubiquitous language、业务规则和不变量。进入代码层面后domain 通常落地为一组类和服务的集合聚合根与实体比如订单聚合根内部管理一组订单项。领域服务处理不适合放在单一实体里的业务逻辑比如“重复下单调价”这种跨越多个对象的规则。领域事件记录业务中真实发生的事情如“订单已支付”供其他领域监听和响应。仓储接口Repository Interface定义如何从外部拿到聚合根的抽象接口具体数据库实现在基础设施层完成。domain 和 entity 更容易混淆的地方在于entity 是 domain 层的组成部分而不是并列关系。domain 包含 entityentity 体现 domain 内部的核心概念。仓库、接口、对外适配层都不属于领域层但围绕领域层运转。在落地时我建议先不要一上来就搞一整套 DDD 组件。对大多数项目先保证“领域层没有基础设施依赖”就够了——领域对象不要引用 Spring 的注解、MyBatis 的注解、JSON 序列化注解让它们像纯净的 Java/Kotlin/TypeScript 类一样存在。这样领域层可以被单元测试完整覆盖后续无论换存储还是换框架核心逻辑都不会被动摇。2.4 一个订单系统的示例对比用一个具体的订单场景把三个概念摆在一起看会更清楚。假设我们要实现“用户创建订单并扣减库存”的功能。Domain这是“订单领域”的规则——创建订单时需要校验用户状态、商品上架状态、库存足不足订单创建后需要记录领域事件。这些规则决定了领域层里有哪些组件。Entity订单本身是聚合根实体订单项也是实体或值对象库存账单如果需要在生命周期内被追踪也可以建模成 entity。Model创建订单接口接收的请求体含用户 ID、商品 ID、数量是请求 model数据库里的订单表映射类订单记录是持久化 model返回给前端的“订单创建成功”信息是响应 model。你发现问题没有一个创建订单的功能至少涉及订单领域、订单实体、请求 model、持久化 model、响应 model 五个不同的东西。如果你用一个Order类全部搞定起步很爽但上线三个月后需求一变改一个字段要反复确认影响范围那时候就知道当初分开写是多么值当。3. 实操过程与核心环节实现3.1 一个可落地的分层代码示例下面给出一个我常用在项目里的简化版订单创建示例用 TypeScript 风格演示领域层、应用层和基础设施层如何分开。核心思路就是domain 层只关心业务model 层负责适配外部输入输出entity 存放状态与行为三者各司其职。// ---------- domain 层 ---------- // 领域规则订单创建时库存要充足 // 注意这里不 import 任何数据库、框架、HTTP 相关的东西 export interface InventoryPort { deduct(productId: string, quantity: number): Promiseboolean; } export class Order { constructor( private readonly id: string, private readonly userId: string, private readonly items: OrderItem[], private status: OrderStatus OrderStatus.CREATED ) {} public confirm(inventory: InventoryPort): boolean { for (const item of this.items) { if (!inventory.deduct(item.productId, item.quantity)) { this.status OrderStatus.CANCELLED; return false; } } this.status OrderStatus.CONFIRMED; return true; } } export class OrderItem { constructor( public readonly productId: string, public readonly quantity: number ) {} } export enum OrderStatus { CREATED CREATED, CONFIRMED CONFIRMED, CANCELLED CANCELLED }// ---------- application / service 层 ---------- // 编排领域对象和外部端口但本身不写具体 SQL 或 HTTP import { Order, InventoryPort } from ./domain/order; export class CreateOrderService { constructor(private readonly inventory: InventoryPort) {} async execute(userId: string, items: { productId: string; quantity: number }[]) { const orderNo generateOrderNo(); const order new Order(orderNo, userId, items); const ok await order.confirm(this.inventory); if (!ok) { throw new Error(inventory not enough); } return order; } }// ---------- infrastructure / repository 层 ---------- // 这里才算真正接触数据库和外部服务 export class InventoryRpcAdapter implements InventoryPort { async deduct(productId: string, quantity: number): Promiseboolean { const resp await axios.post(/api/inventory/deduct, { productId, quantity }); return resp.data.success; } }// ---------- model / DTO 层 ---------- // 对外接口的入参出参和数据库、领域对象都解耦 export interface CreateOrderRequest { userId: string; items: Array{ productId: string; quantity: number }; } export interface CreateOrderResponse { orderId: string; status: string; }这段代码里没有出现一个名为Model的类但每个类其实都可以被对应到某种 model 或 entity 的职责。最关键的分界线是InventoryPort、Order、OrderItem这些和业务规则直接相关的类型放在领域层不依赖任何框架InventoryRpcAdapter属于基础设施它唯一的工作就是把外部系统的调用适配成领域层需要的接口。3.2 避免三层混用的实战技巧很多项目的问题不是没有分层而是分层之后没人遵守。我踩过的坑和对应的解决办法基本可以总结成几条禁止领域对象中出现数据库字段。例如Order类里不要有createdAt这种纯粹由数据库写入的字段。如果你需要展示创建时间可以在持久化映射类entity/DO里补或者由应用层投射出来。禁止 DTO 复用领域对象。接口返回字段经常比领域对象少也可能多比如冗余的展示字段。与其用JsonIgnore一个个忽略字段不如单独建响应 model字段增减都不会影响领域层。所有跨对象业务规则优先放领域服务。判断标准是这个规则如果离开某个实体也能成立那它就属于领域服务或应用服务而不是硬塞进某个 entity 里。比如“下单时赠送优惠券”涉及订单和优惠券两个聚合应该写在领域服务里而不是让Order自己调券服务。一个文件一个角色。不要用Order.java同时表示提交参数、业务对象、数据库记录。哪怕文件多几个代码搜索成本也远低于后期梳理成本。3.3 映射策略从数据库到前端的一次完整流转很多人在实际写代码时卡在“何时把 entity 转成 model”这个问题上。我常用的映射策略是数据库 layer 出 DAO/Entity应用层装配成领域对象应用服务最终再转成 API 响应 model。听上去绕实际就是三步从存储层取持久化对象比如OrderRecord这一步通常由 ORM 完成。组装为领域对象Order把数据库字段映射到业务对象必要时补充业务规则所需的其他数据。返回给调方时把领域对象转成 DTO/VO比如CreateOrderResponse隐藏内部细节。这三步之间需要显式写映射函数不要依赖隐式转换。比如可以用一个OrderMapper专门负责OrderRecord到Order的互转以及Order到CreateOrderResponse的转出。一旦有了这个清晰的映射层后续替换数据库、调整接口响应字段影响都会被限制在很小的范围内。4. 常见问题与排查技巧实录4.1 概念迷雾速查表平时在团队里答疑我总结了一个小表格用来快速判断某个类到底该怎么命名、放哪个层。虽然不是完美理论但很实用特征entitymodelDTO/VOdomain主要作用承载业务身份和状态承载跨层数据传递承载业务规则与边界是否有方法有且行为内聚一般只有属性最多有 getter/setter有实体、服务、事件等是一整套体系是否依赖框架不应依赖具体框架可能依赖序列化注解如 JSON不应依赖任何框架生命周期贯穿业务过程一次调用或一次展示即完概念上长期存在运行时通过对象实例化适合放哪层domain 层application/interface 层领域层本身这张表不能解决所有设计争议但至少能帮你在评审代码时快速对焦一个类如果只有 getter/setter还在方法内部写了复杂业务判断那大概率是放错了位置要么拆出领域方法要么把业务逻辑抽到领域服务。4.2 实际问题排查为什么我改了实体接口就崩了这个问题的根源通常不是实体本身而是所有层都在使用同一个实体类。比如前端要展示折扣价你顺手给订单实体加了一个discountPrice字段结果数据库映射表也读这个字段领域层校验也读这个字段以后一旦有多套来源比如数据库没这列折扣价由促销系统计算得出整个链路就会集体爆炸。排查的思路很简单看这个字段是不是被两处以上的职责共用。如果是说明该拆 model 了。正确的做法是数据库层实体里根本没有discountPrice促销系统通过接口把价格算好应用层合成响应 model 时再加上这个字段前端收到的是一个组合好的 DTO谁都不用迁就谁。另一种常见问题是“领域对象直接序列化给前端”。有时为了省事你会把Order序列化返回结果把内部的inventoryPort、状态枚举之类的实现细节暴露了。轻则泄露内部设计重则因为递归引用导致序列化栈溢出。排查时先看接口返回的 JSON 结构是否包含领域层才有的概念如果是就是在应用层缺少一个响应 model 的映射。4.3 团队协作时统一语言的实用建议代码里充满歧义最痛的还不是实现而是沟通成本。一个功能从产品需求评审到编码如果每个人对“这个 order 指的是什么”都有不同理解那同一段代码被反复推翻重写就不奇怪了。我的建议是在项目里建立一个轻量级的“领域术语表”不一定要写成文档可以先在 README 里列出来再用代码结构约束数据库相关的类带Record后缀如OrderRecord。领域对象不做后缀直接用业务词如Order、OrderItem。接口入参类带Request后缀如CreateOrderRequest。接口出参类带Response后缀如CreateOrderResponse。应用服务类带Service后缀如CreateOrderService。光这个小小命名约定就能消除掉大半的概念混乱。再配合 code review 时强制检查“这个类是否属于它所在层的职责”团队对 entity、model、domain 的认知会越来越一致。4.4 关于“domain”在技术栈中的另一种歧义还要提醒一句在代码里搜“domain”这个词经常会搜到一堆和业务领域无关的东西。比如邮件服务器的域名、数据库的域、Windows 域、API gateway 路由里的 domain 配置。很多报错信息里出现domain is disabled、domain forbidden之类的提示跟你的业务领域设计一点关系都没有别在排查的时候被带偏。做架构设计时先确认你口中的“domain”到底是“业务领域”还是“网络域名/服务器域”以免两个人讨论半天其实说的是两码事。在实际开发中我见过有人因为把“domain 层”理解成“放所有跟域名配置相关的类”结果把业务逻辑堆到了别处也见过后端把网络请求的 domain 配置错误当成领域层问题去查白白浪费了大量时间。概念清晰有时候能直接省下好几个小时的排查时间。5. 从概念到落地重新审视你的项目结构到这里entity、model、domain 三个概念的关系应该清晰了。最后分享一下我每次接手新项目时的检查步骤也当作一个现成的自检清单先看最核心的业务对象类比如订单、用户、商品是否只包含业务行为和业务状态有没有出现 SQL 语句、HTTP 调用、JSON 注解。再看接口层请求和响应参数是否单独建模有没有直接复用数据库映射类。最后看业务规则的位置判断“如果需求变化我要改几层代码”。如果改一个规则要同时动数据库、接口文档和前端字段那就是揉得太紧该拆了。我个人的体会是这三个词的概念之争本质上是“分层思想和对象职责”之争。你不需要背下来哪本书里怎么定义只要代码里能明确区分“有行为和身份的对象entity”“承载数据形状的对象model”以及“业务规则与边界的集合domain”架构就已经比大多数项目健康了。如果你在重构老项目建议不要一次推倒重来。挑一个最核心、改动最频繁的业务模块先做一次类职责盘点把混用的类拆开跑通一整个功能链路后再决定要不要推广到其他模块。架构调整最忌讳的就是感情用事按模块逐步灰度才是真正稳的路子。
返回列表