当前位置: 首页 > article >正文

架构演进之 DDD:从 CRUD 到领域驱动设计

前言每一个贫血模型背后都有一个渐行渐远的业务在软件开发的早期阶段我们通常从一个简单的 CRUD 应用开始。随着业务逻辑日益复杂代码库逐渐膨胀我们开始面临一个普遍的问题业务逻辑散落在各处核心概念模糊不清修改一处牵一发而动全身。这种困境的根源往往在于我们一直沿用着“面向数据库”的编程思维而非“面向业务”的建模思维。领域驱动设计Domain-Driven Design简称 DDD正是为解决这一问题而生。本文将带你回顾从 CRUD 到 DDD 的演进之路分析不同架构模式的优劣并探讨如何在实际项目中逐步引入 DDD。一、CRUD 的困境贫血模型的局限性1.1 典型的 CRUD 架构传统的 CRUD 应用通常采用分层架构Controller → Service → DAO → Database其中Controller接收请求参数校验返回响应Service业务逻辑事务管理DAO/Repository数据访问ORM 操作Entity数据表映射仅有 getter/setter这种架构下Entity 通常是一个“贫血模型”——只包含数据字段和简单的访问方法没有任何业务行为。1.2 一个典型的例子假设我们有一个订单系统业务规则包括订单只能由“待支付”状态转为“已支付”支付后需要扣减库存支付成功后发送通知在 CRUD 模式下代码通常是这样写的// 贫血的 Order 实体 Entity public class Order { Id private Long id; private Long userId; private BigDecimal amount; private String status; // PENDING, PAID, CANCELLED // ... getter/setter 省略 } // Service 层承载了所有业务逻辑 Service public class OrderService { Autowired private OrderRepository orderRepository; Autowired private InventoryService inventoryService; Autowired private NotificationService notificationService; Transactional public void payOrder(Long orderId) { Order order orderRepository.findById(orderId); // 业务规则校验 if (!PENDING.equals(order.getStatus())) { throw new BusinessException(订单状态不正确); } // 状态变更 order.setStatus(PAID); orderRepository.save(order); // 扣减库存 inventoryService.deductStock(order); // 发送通知 notificationService.sendPaymentSuccess(order); } }1.3 CRUD 模式的问题随着业务增长这种模式暴露出一系列问题问题表现业务逻辑泄露核心业务规则被分散在 Service 层不同 Service 可能重复实现相同逻辑领域概念缺失代码中只有“数据表”没有“业务概念”如“支付”只是一个状态字段的修改可维护性差业务规则变更时需要排查所有涉及该实体的 Service 方法测试困难业务逻辑与基础设施耦合单元测试需要大量 mock难以复用领域行为无法被其他服务复用只能复制粘贴二、DDD 的核心思想让领域模型成为核心领域驱动设计由 Eric Evans 在其著作中系统阐述核心思想是当业务复杂度上升时应该将注意力集中在领域本身让领域模型成为软件的核心。2.1 DDD 的战略设计战略设计关注的是宏观层面的划分限界上下文Bounded Context明确定义模型的边界不同上下文可以有同名但含义不同的概念上下文映射Context Mapping定义不同上下文之间的集成关系子域Subdomain将大系统划分为核心域、支撑域、通用域2.2 DDD 的战术设计战术设计提供了实现领域模型的具体工具概念说明实体Entity具有唯一标识和生命周期可变的业务对象值对象Value Object无唯一标识通过属性值定义不可变聚合Aggregate一组相关对象的集合通过聚合根统一访问领域服务Domain Service处理跨多个实体的业务逻辑领域事件Domain Event记录领域中发生的重要事情仓储Repository提供聚合的持久化抽象工厂Factory封装复杂对象的创建逻辑三、DDD 重构从贫血到充血让我们用 DDD 的思路重构上面的订单支付场景。3.1 第一步识别领域概念首先我们需要识别出核心的领域概念订单Order聚合根核心实体订单状态OrderStatus值对象金额Money值对象支付Payment领域事件订单支付服务可能涉及领域服务3.2 第二步构建充血模型// 订单聚合根 - 充血模型 public class Order { private OrderId id; // 值对象作为ID private UserId userId; // 值对象 private Money totalAmount; // 值对象 private OrderStatus status; // 值对象 private ListOrderItem items; // 实体集合 // 聚合根负责保证内部一致性 public void pay() { // 状态校验 - 业务规则内聚在实体中 if (!this.status.canPay()) { throw new DomainException(订单当前状态无法支付); } // 状态变更 this.status OrderStatus.PAID; // 注册领域事件 registerEvent(new OrderPaidEvent(this.id, this.userId, this.totalAmount)); } public void addItem(Product product, int quantity) { // 添加订单项可能涉及业务规则 if (this.status.isPaid()) { throw new DomainException(已支付订单不能添加商品); } OrderItem item new OrderItem(product, quantity); this.items.add(item); this.totalAmount calculateTotalAmount(); } private Money calculateTotalAmount() { return this.items.stream() .map(OrderItem::getSubtotal) .reduce(Money.ZERO, Money::add); } } // 值对象 - 订单状态 public class OrderStatus { private final String value; public static final OrderStatus PENDING new OrderStatus(PENDING); public static final OrderStatus PAID new OrderStatus(PAID); public static final OrderStatus CANCELLED new OrderStatus(CANCELLED); private OrderStatus(String value) { this.value value; } public boolean canPay() { return this PENDING; } public boolean isPaid() { return this PAID; } // 重写 equals/hashCode } // 值对象 - 金额 public class Money { private final BigDecimal amount; private final Currency currency; public Money add(Money other) { if (!this.currency.equals(other.currency)) { throw new IllegalArgumentException(货币单位不一致); } return new Money(this.amount.add(other.amount), this.currency); } // ... 其他方法 }3.3 第三步领域服务与仓储// 仓储接口 - 定义在领域层 public interface OrderRepository { Order findById(OrderId id); void save(Order order); } // 领域服务 - 处理跨聚合的业务逻辑 Service DomainService public class PaymentDomainService { private final OrderRepository orderRepository; private final InventoryService inventoryService; // 防腐层接口 private final DomainEventPublisher eventPublisher; public void handleOrderPaid(OrderPaidEvent event) { // 支付后的跨聚合操作 inventoryService.deductStock(event.getOrderId()); eventPublisher.publish(event); } } // 应用层 - 编排领域对象 Service public class OrderApplicationService { private final OrderRepository orderRepository; private final PaymentDomainService paymentService; Transactional public void payOrder(PayOrderCommand command) { Order order orderRepository.findById(command.getOrderId()); // 业务逻辑在领域对象内部 order.pay(); orderRepository.save(order); // 处理领域事件 order.getEvents().forEach(paymentService::handleOrderPaid); } }四、架构对比两种思维模式维度CRUD 模式DDD 模式核心关注点数据如何存储业务如何运转模型特征贫血模型只有数据充血模型数据行为业务逻辑位置Service 层实体/值对象/领域服务封装性差业务规则暴露在外好通过聚合根保证一致性代码表达力技术实现导向业务概念导向适合场景简单 CRUD、管理后台复杂业务逻辑、核心业务系统五、演进策略如何逐步引入 DDD对于已有系统全面重构为 DDD 往往风险较高。推荐采用渐进式演进策略5.1 第一步识别核心域从业务价值最高的模块开始。核心域是公司核心竞争力所在最值得投入。5.2 第二步定义限界上下文明确新模块的边界避免与现有系统过度耦合。可以建立防腐层Anti-Corruption Layer隔离旧系统。5.3 第三步构建领域模型与领域专家深度合作用事件风暴Event Storming等方法探索领域知识建立统一的领域语言Ubiquitous Language。5.4 第四步逐步替换新功能直接用 DDD 实现旧功能在修改时逐步重构建立领域层与应用层分离的清晰边界5.5 演进过程中的注意事项不要为了 DDD 而 DDD简单系统用 CRUD 完全够用保持务实不是所有概念都需要值对象不是所有实体都需要工厂重视团队能力DDD 对团队的业务理解能力和设计能力要求较高基础设施不影响领域仓储接口定义在领域层实现放在基础设施层六、DDD 的适用场景DDD 并非银弹它有自己的适用边界适合采用 DDD 的场景业务逻辑复杂规则多变需要长期维护和演进的核心系统团队具备领域建模能力有领域专家可以深度协作不适合采用 DDD 的场景简单的 CRUD 应用技术原型或短期项目团队缺乏领域建模经验业务逻辑简单以数据展示为主七、结语回到业务本身从 CRUD 到 DDD本质上是一种思维方式的转变从“如何存储数据”转向“如何表达业务”。DDD 的实践确实会带来一定的复杂度但当业务足够复杂时这种复杂度是值得的。它帮助我们建立团队与业务方的共同语言让代码成为业务的忠实表达在业务变化时保持系统的可演进性正如 Eric Evans 所说“领域建模不是一项技术活动而是一项知识探索活动。”当我们真正理解业务并让代码反映这种理解时我们就走出了 CRUD 的困境进入了领域驱动的世界。演进之路没有终点只有持续的探索与优化。愿我们在每一次重构中离业务本质更近一步。

相关文章:

架构演进之 DDD:从 CRUD 到领域驱动设计

前言:每一个贫血模型背后,都有一个渐行渐远的业务在软件开发的早期阶段,我们通常从一个简单的 CRUD 应用开始。随着业务逻辑日益复杂,代码库逐渐膨胀,我们开始面临一个普遍的问题:业务逻辑散落在各处&#…...

003 TimeTagger 时间跟踪工具本地部署与开机自启

TimeTagger 本地部署与开机自启手册 一、TimeTagger 产品介绍 TimeTagger 是一款轻量级、开源免费的Web端时间跟踪工具,基于PythonWeb技术开发,支持本地离线运行、时间记录分类、数据统计分析,无需云端账号即可实现数据本地持久化存储&#…...

HMC5883L磁力计驱动开发与航向解算实战

1. HMC5883L电子罗盘芯片技术解析与嵌入式驱动开发实践HMC5883L是由Honeywell公司推出的三轴磁力计(Magnetometer),专为高精度地磁方向检测而设计。尽管其已逐步被更新型号(如QMC5883L、AK09916等)替代,但在…...

别再暴力删括号了:一道“删除无效括号”,看懂搜索本质与剪枝思维

别再暴力删括号了:一道“删除无效括号”,看懂搜索本质与剪枝思维 大家好,我是Echo_Wish。 今天聊一道很多人“看着简单,写起来崩溃”的经典题—— 👉 删除无效的括号(Remove Invalid Parentheses) 这题我第一次做的时候,说实话,脑子里只有一个想法: 👉 “不就…...

Audio Pixel Studio新手指南:MP3/WAV/OGG多格式兼容性测试与编码建议

Audio Pixel Studio新手指南:MP3/WAV/OGG多格式兼容性测试与编码建议 1. 认识Audio Pixel Studio Audio Pixel Studio是一款基于Streamlit开发的轻量级音频处理Web应用,专为需要快速处理音频内容的创作者设计。它集成了两大核心功能:Edge-T…...

BBDown:告别在线观看限制,打造你的专属B站离线视频库

BBDown:告别在线观看限制,打造你的专属B站离线视频库 【免费下载链接】BBDown Bilibili Downloader. 一款命令行式哔哩哔哩下载器. 项目地址: https://gitcode.com/gh_mirrors/bb/BBDown 你是否曾经遇到过这样的情况:想要收藏B站上的精…...

Java集合入门必看:ArrayList与LinkedList常用方法详解(附实战案例)

在Java开发中,集合是日常使用频率极高的工具,而ArrayList和LinkedList作为List接口的两大核心实现类,分别适用于不同的业务场景。很多初学者在使用时,常常混淆两者的方法用法,不清楚何时该用ArrayList、何时该用Linked…...

本地大模型系列:2.通过API让本地大模型为你服务

上一篇我们介绍了ollama和lmstudio,这两个工具都是个人版的运行大模型的工具,可以用来运行本地小参数的LLM(所谓小参数一般指不超过27B的4bit量化LLM,按照1B参与大约需要0.7G显存加载进行预测,加上其他开销月1-1.5G&am…...

Spring AI Alibaba MCP协议实战:模型上下文协议集成与工具调用

Spring AI Alibaba MCP协议实战:模型上下文协议集成与工具调用 导读:MCP(Model Context Protocol,模型上下文协议)是 Anthropic 于 2024 年末发布的开放标准,旨在解决 AI 工具调用的碎片化问题。Spring AI …...

嵌入式信号发生器库:高精度方波生成与载波调制

1. SignalGenerator 库概述SignalGenerator 是一个轻量级、可移植的嵌入式信号发生器开源库,专为资源受限的微控制器设计。其核心目标是在任意 GPIO 引脚上生成高精度、可编程的方波信号,同时支持载波调制(Carrier Modulation)功能…...

XPath 语法完全指南:从基础语法到 SQL 注入中的应用

爬虫、处理过配置文件、或者接触过 MySQL 的 XML 函数的一定都见过 XPath 这个名词。它是一种专门用来在 XML 文档中“寻址”的语言,语法像文件路径一样直观。同时,XPath 是 Web 安全领域中 SQL 报错盲注中的关键角色。一、XPath 是什么?XPat…...

南北阁 Nanbeige 4.1-3B 企业应用方案:私有化部署+对话记忆管理+审计日志扩展接口

南北阁 Nanbeige 4.1-3B 企业应用方案:私有化部署对话记忆管理审计日志扩展接口 想在企业内部部署一个既智能又安全的对话AI吗?今天我们来聊聊如何基于南北阁 Nanbeige 4.1-3B 模型,打造一个功能完备的企业级对话应用。这个方案不仅支持纯本…...

保姆级教程:用Python+Robotics Toolbox搞定Panda机械臂的DH建模与正逆解(附避坑指南)

从零实现Panda机械臂运动学:Python Robotics Toolbox实战指南 机械臂控制是机器人领域的核心技术之一,而运动学建模则是实现精准控制的基础。本文将带您使用Python的Robotics Toolbox,一步步完成Franka Emika Panda机械臂的DH参数建模、正运动…...

【JSReverser-MCP】一句话逆向猿人学21题

近期听闻AI可以做逆向了,于是赶紧来试一试。 相关参考: vibe coding 氛围编程 Vibe coding 用 AI 做 JS 逆向食用教程 JSReverser-MCP 源:https://github.com/NoOne-hub/JSReverser-MCP 安装 环境要求 nodejs v22及以上 已部署Claude code或codex…...

GLM-TTS语音克隆实测:5分钟搞定方言克隆,效果惊艳!

GLM-TTS语音克隆实测:5分钟搞定方言克隆,效果惊艳! 1. 引言:语音克隆技术的新突破 在数字内容爆炸式增长的今天,个性化语音合成技术正成为内容创作者、企业客服、教育机构等领域的刚需。传统语音克隆方案往往需要数小…...

Adafruit ICM20X库详解:ICM20649与ICM20948驱动开发指南

1. 项目概述 Adafruit ICM20X 是 Adafruit 官方维护的 Arduino 兼容库,专为 TDK InvenSense 公司推出的 ICM20649 与 ICM20948 两款高性能 MEMS 运动传感器设计。该库并非通用型 ICM20X 系列驱动,而是聚焦于 Adafruit 自行设计并销售的硬件模块——即 I…...

计算机视觉进阶教学之Mediapipe库(一)

目录 简介 一、Mediapipe Python的安装和应用 二、手部检测 1. 导入必要的库 2. 初始化 MediaPipe 组件 3. 配置并创建手部检测模型实例 4. 启动摄像头并进入主循环 5. 绘制检测结果 三、手势识别 1. 手势识别核心逻辑 a. 计算基准距离 (Base Distance) b. 计算各手…...

Alpamayo-R1-10B开源模型价值:降低L4研发门槛,让算法团队专注因果逻辑而非工程胶水

Alpamayo-R1-10B开源模型价值:降低L4研发门槛,让算法团队专注因果逻辑而非工程胶水 1. 项目概述 Alpamayo-R1-10B是NVIDIA推出的自动驾驶专用开源视觉-语言-动作(VLA)模型,作为一套完整的自动驾驶研发工具链&#xf…...

SmolVLA参数详解:256×256输入分辨率对边缘计算设备的友好性分析

SmolVLA参数详解:256256输入分辨率对边缘计算设备的友好性分析 1. 引言:为什么边缘设备需要紧凑的视觉语言动作模型 在机器人技术和边缘计算领域,我们经常面临一个核心矛盾:强大的AI能力需要大量计算资源,而边缘设备…...

论文AI率突然从20%涨到50%怎么办?紧急处理攻略

论文AI率突然从20%涨到50%怎么办?紧急处理攻略 上周五,一个学妹发消息给我,语气挺崩溃的:“学长,我的论文上个月查AI率才18%,今天重新查直接变成52%了,后天就要交终稿,怎么办啊&…...

密码学算法 - 连分数算法

当你在计算某个数的近似值时🔍,或者在求解某个方程的根时🧮,连分数算法 就像一把神奇的放大镜🔎,能帮你逐步逼近那个隐藏在数字背后的真相。 欢迎来到《密码学核心算法实战》的连分数专题!这里…...

Linux内核构建三要素:Makefile、Kconfig与.config协同机制

1. Linux内核构建系统核心机制解析:Makefile、Kconfig与.config的协同关系在嵌入式Linux开发实践中,内核编译常被视为一道技术门槛。开发者面对庞大的源码树(以Linux-3.4.2为例,包含超过2.5万文件),往往陷入…...

DIY四足机器人入门:用开源项目打造你的第一个仿生机器狗

DIY四足机器人入门:用开源项目打造你的第一个仿生机器狗 四足机器人正从实验室走向创客空间。想象一下,周末午后在自家工作台上组装出一台能行走、奔跑甚至跳舞的机器狗——这不再是科幻场景。得益于MIT Mini Cheetah等开源项目的出现,普通爱…...

HAR实战指南:从Kinetics-400数据集获取到视频帧预处理全流程解析

1. Kinetics-400数据集入门指南 第一次接触Kinetics-400数据集时,我被它庞大的规模震撼到了。这个包含40万段视频片段的数据集,涵盖了从"打篮球"到"刷牙"等400种日常动作,是训练人体动作识别(HAR)模型的黄金标准。但随之…...

通义千问3-Reranker-0.6B模型压缩:基于TensorRT的推理加速

通义千问3-Reranker-0.6B模型压缩:基于TensorRT的推理加速 1. 为什么需要对Qwen3-Reranker做TensorRT优化 你可能已经试过直接加载Qwen3-Reranker-0.6B跑重排序任务,但很快会发现:推理速度不够快,显存占用偏高,尤其在…...

2026年AI提示词(Prompt)终极指南:国内聚合站实战技巧

在AI应用日益深入的2026年,能否写出高质量的提示词(Prompt),直接决定了你从GPT、Claude、Gemini等大模型中获得价值的效率与深度。对于国内用户,学习和实践提示词工程的最佳平台是聚合型AI镜像站。目前,通过…...

大模型学习笔记------SAM模型架构拆解与实战指南

1. SAM模型架构全景拆解 第一次接触SAM模型时,我被它"分割一切"的野心震撼到了。这就像给计算机视觉领域扔下了一颗原子弹——传统需要专门训练的分割任务,现在通过提示词就能实时完成。在实际部署医疗影像标注系统时,我发现理解这…...

读了Linux内核slab源码,发现Linus在20年前就写出了比std::pmr更高效的内存池——内核内存管理的4个设计模式

一、C++的内存池,和内核比差在哪? C++程序员对内存管理不陌生。从最基本的new/delete,到C++17引入的std::pmr::polymorphic_allocator,再到各种第三方库的对象池实现,我们一直在和内存分配器打交道。 但你有没有想过一个问题:为什么C++标准库直到2017年才搞出std::pmr这…...

Dify + Celery + Webhook深度集成:构建高可靠异步管道的6大关键配置点

第一章:Dify自定义节点异步处理的核心架构演进Dify 自 v0.6.10 起将自定义节点(Custom Node)的执行模型从同步阻塞式全面转向基于事件驱动的异步处理架构,其核心目标是解耦节点执行与工作流调度,提升高并发场景下的资源…...

JTAG接口原理、失效诊断与硬件防护实战指南

1. JTAG接口原理与工程实践深度解析在嵌入式系统与可编程逻辑器件的开发流程中,JTAG(Joint Test Action Group)接口不仅是调试与烧录的核心通道,更是硬件工程师验证设计完整性、定位物理层故障的关键工具。本文基于FPGA开发中频繁…...