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

DDD是AI编程-上下文工程的良好框架

DEMO(所谓的氛围编程),但几乎不可维护。

然后我不得不重构项目,并开始思考:什么是AI介入后端的良好范式?

就这样我不由自主的走上了领域驱动设计(DDD)的路线,以下是我的一点探索和浅见。

在我看来,DDD 是后端项目负责人想要掌控项目和IT企业想要掌控核心资产的黄金实践。DDD理想情况下需要项目负责人具备领域专家的业务能力,它更适合那种在垂直领域扎根并依赖底层技术创新来扩展业务的项目。现在它也适合AI辅助编程,当然也可以通过AI辅助、迭代调整来逐步建立领域知识——关键是要有"追求理解业务本质"的意识。

依赖倒置:让业务逻辑不受框架绑架

传统三层架构的依赖链条:

UI层 → 业务逻辑层 → 数据访问层 → 数据库

换个ORM框架?改业务代码。数据库从MySQL换PostgreSQL?改业务代码。业务逻辑散落各层?到处都要改。中间的业务逻辑就是块夹心饼干,干脆敞开了,模型谁都可以用,跨层调用?随便,怎么方便怎么来。

DDD通过依赖倒置,让领域层定义接口,基础设施层去实现:

// 领域层定义接口(不依赖任何框架) namespace NamBlog.Domain.Interfaces { public interface IPostRepository { Task<Post?> GetByIdAsync(int postId); Task SaveAsync(Post post); } } // 基础设施层实现(可以随意切换实现) namespace NamBlog.Infrastructure.Persistence { public class EFPostRepository : IPostRepository // 用EF Core实现 { private readonly BlogContext _context; // ... } // 将来可以换成 Dapper、MongoDB、内存存储,领域层代码不变 }

好处:

  • 领域层只依赖核心的语言特性和开发框架,长期稳定,减少技术升级的成本,也可以独立测试。
  • 换实现框架只需修改基础设施层。
  • .NET的领域模型可以被Go、Rust等其他语言平台重新实现(业务规则复用),就像乐高积木可以在不同场景中复用核心模块。

代码即文档:充血模型的自解释性

在我的NamBlog项目中,如果你问"文章能做哪些操作?",不需要翻文档,看Post类就够了:

public class Post { // 10个业务方法穷举了所有关键业务操作 public void UpdateMetadata(...) // 改元数据 public PostVersion SubmitNewVersion() // 提交新版本 public void Publish(int versionId) // 发布 public void Unpublish() // 取消发布 public void Feature() // 设为精选 // ... }

对比贫血模型

// 贫血模型:Post只是数据容器 public class Post { public int Id { get; set; } public bool IsPublished { get; set; } // 谁都能改 } // 业务逻辑散落在各个Service里 public class PostService { public void Publish(int postId) { ... } // 在这 } public class ArticleManager { public void PublishArticle(int id) { ... } // 还在这? } // 3个月后自己都不记得怎么回事

充血模型的好处是:看代码就知道能做什么,不能做什么。

整洁架构与六边形架构:殊途同归

DDD经常和整洁架构、六边形架构一起提到,它们的核心思想是一致的:

六边形架构

外部系统(数据库、API、UI)→ 适配器层 → 端口/接口 → 核心业务逻辑

整洁架构

同心圆结构: 外层 → 框架&驱动→ 接口适配器 → 应用业务规则 内层 → 企业业务规则

在NamBlog中的映射:

基础设施层(Infrastructure) → 六边形的"适配器"、整洁架构的"框架层" 领域接口(Interfaces) → 六边形的"端口" 领域层(Domain) → 整洁架构的"企业业务规则" 应用层(Application) → 整洁架构的"用例层"

核心都是:业务规则在最内层,不依赖外部任何东西。外部世界可以随便换,业务规则不动。

我的实践:NamBlog的领域设计

说明:文中代码示例为了便于理解做了适当简化,完整实现请参考 GitHub 仓库

从业务出发而不是数据库

NamBlog的核心业务流程:

写作者创建Markdown → AI转换成HTML → 管理多个版本 → 选择发布

一开始我也习惯从数据库设计开始,但转念一想:业务的核心是什么?

3个关键的业务规则:

  1. 一篇文章可以有多个版本,但同时只能发布1个版本
  2. Markdown是文章本体,HTML是派生物
  3. 发布/取消发布不影响版本历史(随时可回溯)

这些规则怎么体现在代码中?

public class Post // 聚合根 { // 规则1: MainVersionId是单值,确保同时只有一个已发布版本 public int? MainVersionId { get; private set; } public PostVersion? MainVersion { get; private set; } // 规则2: Versions集合保存所有历史 public ICollection<PostVersion> Versions { get; private set; } // 规则3: 发布状态独立于版本 public bool IsPublished { get; private set; } public DateTimeOffset? PublishedAt { get; private set; } // 业务方法封装操作 public void Publish(int versionId) { var version = Versions.FirstOrDefault(v => v.VersionId == versionId); if (version == null) throw new DomainException("版本不存在"); if (string.IsNullOrEmpty(Slug)) throw new DomainException("Slug不能为空"); MainVersionId = versionId; IsPublished = true; PublishedAt = DateTimeOffset.UtcNow; } public void Unpublish() { IsPublished = false; // 注意:MainVersionId保留,可以快速重新发布同一版本 } }

关键点:

  • private set防止外部绕过业务规则
  • 业务方法中的检查保护了不变式
  • 看代码就能理解业务:发布需要版本ID和Slug,取消发布保留主版本

两个工厂方法:业务场景的区分

NamBlog支持两种创建文章的方式:

// Web编辑器:用户手动创建 public static Post CreateFromUserInput( string fileName, string author, string? category) { return new Post( fileName: fileName, filePath: string.Empty, author: author, category: category ); } // Obsidian同步:文件系统监控 public static Post CreateFromFileSystem( string fileName, string? filePath, string author) { return new Post( fileName: fileName, filePath: filePath, // 保留文件路径 author: author, category: filePath ?? "Unclassified" // 路径即分类 ); }

为什么要两个工厂方法?

最初我想用一个方法+参数区分,但后来发现业务语义不同:

  • Web创建:立即可编辑,分类由用户选
  • 文件同步:路径即分类,支持文件夹层级

如果用贫血模型,这些差异会藏在Service的if-else里,很难直观理解为什么这样判断。

务实的妥协

理想的DDD领域层应该完全独立于框架,但DDD也具有灵活性,NamBlog作为MVP,我做了妥协:

// 领域层:没有任何框架依赖 public class Post { public int PostId { get; private set; } // 纯C#属性 public void Publish(int versionId) { ... } // 业务方法 } // 基础设施层:用Fluent API配置映射,没用自己的POCO public class PostConfiguration : IEntityTypeConfiguration<Post> { public void Configure(EntityTypeBuilder<Post> builder) { builder.HasKey(p => p.PostId); builder.HasIndex(p => p.Title).IsUnique(); // ... } }

原则是,领域层有没有外层依赖?如果没有,妥协是合理的。

  • ✅ 可以接受:ORM注解、导航属性
  • ❌ 不能接受:在领域方法里写SQL、依赖DbContext

总结DDD容易踩的坑

坑1:没理清业务边界导致理解偏差

一开始我把"文章版本"(PostVersion)设计为值对象,理由是"它依附于Post存在"。结果写了一半发现:

  • 版本需要独立的ID(用户要切换版本)
  • 版本有自己的生命周期(可以被删除)
  • 版本需要关联AI生成记录

我混淆了"依附关系"和"实体/值对象"的判断标准。

正确判断:

值对象:没有唯一标识,通过属性判断相等 实体: 有唯一标识,即使属性相同也是不同对象 PostVersion有VersionId → 是实体 就算两个版本内容完全相同,也是两个不同的版本

教训:不要想当然,要问自己"这个东西需要被独立识别吗?"

坑2:命名不规范引发误解

反面案例:

// 糟糕的命名 public class Post { public void Change(int id) { ... } // 改什么?版本?状态? public void Update() { ... } // 更新什么? public void Process() { ... } // 处理什么业务? }

好的命名应该体现业务意图:

public class Post { public void Publish(int versionId) // 清楚:发布指定版本 public void UpdateMetadata(...) // 清楚:只改元数据 public void SubmitNewVersion() // 清楚:提交新版本 }

我的经验:方法名应该让AI(或产品经理)能秒懂。如果你写的方法名需要技术背景才能理解,可能就有问题。

坑3:过度设计验证规则

案例:

// 过度设计 public class Slug : ValueObject { private readonly string _value; private Slug(string value) { _value = value; } public static Slug Create(string value) { if (string.IsNullOrEmpty(value)) throw ... if (!Regex.IsMatch(value, "^[a-z0-9-]+$")) throw ... return new Slug(value); } protected override IEnumerable<object> GetEqualityComponents() { yield return _value; } }

为了一个验证逻辑,写了20行代码。在MVP阶段,这是过度设计。

我的做法:

public class Post { public string? Slug { get; private set; } public void UpdateMetadata(string? slug = null, ...) { if (slug != null) { ValidateSlug(slug); // 验证逻辑放在这里 Slug = slug; } } private static void ValidateSlug(string slug) { if (!ValidationRuleset.Post.Slug.IsValid(slug)) throw new ArgumentException( ValidationRuleset.Post.Slug.GetValidationError(slug, "Slug"), nameof(slug)); } }

何时用值对象? 当它在多个聚合中复用,且有复杂的行为时。否则简单属性+验证方法就够了。

坑4:忽略了业务语言

DDD强调"统一语言"(Ubiquitous Language),但我一开始没太重视。

反面案例:

// 我的早期代码 public void SetMainVersion(int versionId) { ... } // "什么叫'设置主版本'?

修正后:

public void Publish(int versionId) { ... } // 使用业务术语

教训:代码中的术语应该和产品、运营、业务人员的语言一致。如果他们说"发布",你就别写SetMainVersion

坑5:聚合边界划分不清

困惑:评论(Comment)应该是Post的一部分,还是独立聚合根?

错误思路:"评论依附于文章,所以应该是Post的子实体"

// 错误设计 public class Post { public ICollection<Comment> Comments { get; set; } // 文章加载就加载所有评论? }

问题:

  • 文章有1000条评论,每次加载Post都要加载1000条评论?
  • 删除评论需要先加载Post,修改集合,再保存整个Post?
  • 评论的并发控制怎么办(多人同时评论)?

正确做法:评论是独立聚合根(如果需要的话)

public class Comment // 独立聚合 { public int CommentId { get; private set; } public int PostId { get; private set; } // 只引用Post的ID,不是导航属性 public void Delete() { IsDeleted = true; // 独立操作,不影响Post } }

我一开始也想加评论功能,但后来决定不在后端实现,原因是:

  1. NamBlog的核心是"用AI生成精美HTML",评论会破坏HTML的原汁原味
  2. 如果用户需要,可以在提示词中让AI生成时内嵌评论系统
  3. 避免聚合边界的复杂性,保持Post聚合根的纯粹性

判断标准:

  • 能否独立于父实体进行操作?(能 → 独立聚合)
  • 加载父实体时是否总是需要子实体?(否 → 独立聚合)
  • 子实体的数量是否无上限?(是 → 独立聚合)

DDD与AI编程:一些体会

为什么不应该写大量的中间文档

很多人刚开始用AI编程时,会习惯写大量文档——觉得这样能解释清楚、约束严格一点。AI开发工具(如Copilot)也倾向于生成大量文档。

问题在于:

  1. 时间成本高:写和读这些文档花费大量时间
  2. 文档会过期:代码一改,文档就作废了,不但没有参考价值,反而成为干扰
  3. 上下文污染:AI会因为过时文档中的错误生成更多错误内容,幻觉加剧

这也许是为什么项目做着做着感觉"AI变蠢了"的原因之一——不是AI变蠢了,是上下文被污染了。

DDD的优势:代码即文档。这也是不懂编程的小白和开发人员的最大区别——后者可以不依赖文档用结构更严谨的代码和AI沟通。

为什么RAG索引代码不如索引领域模型

让AI基于项目文档生成代码,问题很大:

文档可能的问题:

"发布文章的流程" 可能在3个文档中: - 需求文档:点击发布按钮后... - 数据库设计:published字段设为true... - API文档:POST /api/posts/{id}/publish... RAG检索到这3段,AI混淆了层次,生成四不像的代码

领域模型的优势:

// AI只需要看这个 public class Post { public void Publish(int versionId) { // 所有发布的业务规则都在这里 } }

上下文清晰、边界明确,AI幻觉大幅减少。

DDD如何约束AI生成的代码

场景对比:**让AI实现文章发布功能

传统方式:

//你: "实现文章发布功能" //AI生成: public void PublishPost(int postId) { var post = _db.Posts.Find(postId); post.IsPublished = true; // 忘记检查版本 _db.SaveChanges(); }

DDD方式:

//你: "实现Post.Publish(int versionId)方法" //AI生成: public void Publish(int versionId) { var version = Versions.FirstOrDefault(v => v.VersionId == versionId); if (version == null) throw ... // 方法签名约束了必须考虑版本 MainVersionId = versionId; IsPublished = true; }

差异:

  • 方法签名就是约束(必须传versionId)
  • private set阻止AI写出post.IsPublished = true这种绕过业务规则的代码
  • AI只能在方法体内实现细节

和AI探讨业务的正确姿势

即便是业务领域专家,也不太可能一开始就掌握了业务的所有细节,肯定要边实现边调整的。AI一开始就可以介入项目开发,从充血模型设计开始,把业务聊通,聊透。但需要注意的,每个人对业务的理解是不一样的,AI也不是专家,它能一下子拿出10个看起来很对的主意,而且无缝在这些主意间切换,所以还要靠人自己的洞察。

❌ 错误:让AI当领域专家

你: "帮我设计博客的领域模型" AI: "建议Post聚合根包含PostMetadata值对象、PostContent实体..." 你: "听起来很专业!" → AI给的是教科书式过度设计

✅ 正确:用AI验证你的想法

你: "文章有两种创建方式: 1. Web编辑器 - 用户立即可编辑 2. Obsidian同步 - 文件路径作为分类 应该用不同的工厂方法吗?" AI: "是的,建议CreateFromUserInput和CreateFromFileSystem" 你: "但Obsidian用户可能没设分类,需要默认值" → 你在澄清业务规则,AI在帮你验证设计

总结个人体会

DDD不是银弹,掌握精髓最重要

我的NamBlog并不是"纯正的DDD":

http://www.jsqmd.com/news/569153/

相关文章:

  • Flowise无障碍服务:视障用户语音交互+触觉反馈指令生成工作流
  • Phi-3-mini-4k-instruct-gguf应用场景:法律文书要点提取、医疗科普内容简化、政务通知转述
  • **发散创新:基于隐私沙盒的Web应用数据隔离机制实战解析**在现代浏览器生态中,**隐私保护已成
  • 从纸质地图到动态GIS:手把手教你用Python+Folium制作交互式专题地图(附代码)
  • 告别内存打架:在STM32项目里优雅使用__attribute__((section))指定变量地址
  • LC-MS非靶向代谢组学实战:从样本处理到Biomarker发现的完整避坑指南
  • Graphormer镜像免配置优势:省去torch-geometric编译、OGB数据集下载等步骤
  • Vivado ILA抓取模拟信号波形?手把手教你用Analog设置替代缺失的Real格式
  • 别再怪工具了!解决蚁剑和哥斯拉连接失败的终极思路:从公司WiFi到手机热点的实战排查
  • HeyGem数字人视频批量生成实战:从上传到下载全流程解析
  • 技术迭代下B端拓客:号码核验的行业进化与价值回归,氪迹科技法人股东号码筛选系统,阶梯式价格
  • CTF逆向实战:手把手教你识别并爆破TEA算法变种(附Python脚本)
  • Qwen3-ASR-1.7B多说话人识别效果展示:会议录音分角色转写
  • Cohere开源20亿参数语音模型:支持14种语言实时转录
  • 用WinHex手把手教你“解剖”U盘:从MBR到FAT表,看懂文件系统底层存储
  • **发散创新:基于Python的Notebook开发新范式——从数据探索到自动化部署的一站式实践**在现代数据
  • 2026年正规资质的鼎湖区用友/高要区用友/金利用友企业用户推荐榜 - 品牌宣传支持者
  • Qwen3-ASR-0.6B创新应用:Token经济语音交互系统
  • 从卫星数据到故障预警:聊聊MAG模型在工业时序异常检测中的迁移实战
  • Gemma-3-12B-IT人工智能应用开发:从理论到实践
  • 告别轮询!用STM32F407的USART3+DMA+空闲中断实现高效串口数据接收
  • 保姆级教程:用Python+Spectral库可视化9个经典高光谱数据集(附完整代码与数据集下载)
  • OSTrack目标跟踪模型初体验:用我的旧笔记本在Win11上实测速度与精度
  • Spring Boot版本升级避坑指南:如何利用Enterprise Support延长维护周期
  • 2026年热门的嘉兴充绒机/全自动充绒机实力公司盘点 - 品牌宣传支持者
  • ChatGPT火爆背后,23个AI术语让你秒懂「龙虾」,避开使用陷阱!
  • intv_ai_mk11效果实测:电商运营人员用AI日均产出文案量提升5倍
  • 避开深沟槽工艺的“坑”:从DLTS数据到TCAD仿真的硅光电二极管陷阱态优化实战
  • 别再傻傻分不清了!ESP-PROG上Program和JTAG接口到底怎么用?手把手教你给ESP32-S3-WROOM-1烧录固件
  • tao-8k部署教程|Xinference模型元数据配置、embedding维度校验与API标准化