DEMO所谓的氛围编程但几乎不可维护。然后我不得不重构项目并开始思考什么是AI介入后端的良好范式就这样我不由自主的走上了领域驱动设计DDD的路线以下是我的一点探索和浅见。在我看来DDD 是后端项目负责人想要掌控项目和IT企业想要掌控核心资产的黄金实践。DDD理想情况下需要项目负责人具备领域专家的业务能力它更适合那种在垂直领域扎根并依赖底层技术创新来扩展业务的项目。现在它也适合AI辅助编程当然也可以通过AI辅助、迭代调整来逐步建立领域知识——关键是要有追求理解业务本质的意识。依赖倒置让业务逻辑不受框架绑架传统三层架构的依赖链条UI层 → 业务逻辑层 → 数据访问层 → 数据库换个ORM框架改业务代码。数据库从MySQL换PostgreSQL改业务代码。业务逻辑散落各层到处都要改。中间的业务逻辑就是块夹心饼干干脆敞开了模型谁都可以用跨层调用随便怎么方便怎么来。DDD通过依赖倒置让领域层定义接口基础设施层去实现// 领域层定义接口不依赖任何框架 namespace NamBlog.Domain.Interfaces { public interface IPostRepository { TaskPost? 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个版本Markdown是文章本体HTML是派生物发布/取消发布不影响版本历史随时可回溯这些规则怎么体现在代码中public class Post // 聚合根 { // 规则1: MainVersionId是单值确保同时只有一个已发布版本 public int? MainVersionId { get; private set; } public PostVersion? MainVersion { get; private set; } // 规则2: Versions集合保存所有历史 public ICollectionPostVersion 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 : IEntityTypeConfigurationPost { public void Configure(EntityTypeBuilderPost 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 IEnumerableobject 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 ICollectionComment 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 } }我一开始也想加评论功能但后来决定不在后端实现原因是NamBlog的核心是用AI生成精美HTML评论会破坏HTML的原汁原味如果用户需要可以在提示词中让AI生成时内嵌评论系统避免聚合边界的复杂性保持Post聚合根的纯粹性判断标准能否独立于父实体进行操作能 → 独立聚合加载父实体时是否总是需要子实体否 → 独立聚合子实体的数量是否无上限是 → 独立聚合DDD与AI编程一些体会为什么不应该写大量的中间文档很多人刚开始用AI编程时会习惯写大量文档——觉得这样能解释清楚、约束严格一点。AI开发工具如Copilot也倾向于生成大量文档。问题在于时间成本高写和读这些文档花费大量时间文档会过期代码一改文档就作废了不但没有参考价值反而成为干扰上下文污染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; }差异方法签名就是约束必须传versionIdprivate 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