领域驱动设计与模块化单体实战术语全解MyMeetings 仓库架构词汇表深度指南【免费下载链接】modular-monolith-with-dddFull Modular Monolith application with Domain-Driven Design approach.项目地址: https://gitcode.com/GitHub_Trending/mo/modular-monolith-with-ddd本文是一份面向 .NET 开发者的领域驱动设计DDD术语实战指南。它以 modular-monolith-with-ddd 仓库docs/catalog-of-terms/目录下的完整术语表为主线逐一讲解 Aggregate、Entity、Value Object、Domain Event、Command、Decorator、Strategy、Dependency Injection 等核心概念的定义、源码实现与业务场景并对照真实代码给出可验证的落地方案。读完本文你将能把这些模式直接映射到自己的模块化单体项目中理解 MyMeetings 中模块内部富领域模型 模块间事件驱动的整体设计思路。MyMeetings 模块化单体架构中模块级别结构示意一、术语表的定位一份可点击学习的 DDD 词汇索引在深入具体模式之前先了解这份术语表本身的结构。docs/catalog-of-terms/README.md是一份带超链接的索引将 DDD、CQRS、事件驱动与软件工程中约 80 个高频术语集中列出。其中有相当一部分术语配有独立子目录内含定义Definition— 模型图Model— 代码示例Code— 解读Description四段式卡片例如docs/catalog-of-terms/Aggregate-DDD/README.mddocs/catalog-of-terms/Command/README.mddocs/catalog-of-terms/Decorator-Pattern/README.mddocs/catalog-of-terms/Dependency-Injection/README.mddocs/catalog-of-terms/Domain-Event/README.mddocs/catalog-of-terms/Entity-DDD/README.mddocs/catalog-of-terms/Event/README.mddocs/catalog-of-terms/Event-Sourcing/README.mddocs/catalog-of-terms/Event-Storming/README.mddocs/catalog-of-terms/Integration-Event/README.mddocs/catalog-of-terms/Strategy-Pattern/README.mddocs/catalog-of-terms/ValueObject-DDD/README.md每张卡片还配有对应的 PlantUML 源文件如docs/catalog-of-terms/Aggregate-DDD/aggregate-ddd.puml体现Diagram as text——模型图本身也是可版本化、可评审的文本资产。而docs/architecture-decision-log/下的 ADR 文件则记录了这些术语对应的架构决策例如0007-use-cqrs-architectural-style.md、0011-create-rich-domain-models.md、0012-use-domain-driven-design-tactical-patterns.md。阅读术语表时配合 ADR可以看到模式名词与项目决策之间的一一对应关系。下面按主题分组深入展开这些术语在 MyMeetings 中的真实实现。二、DDD 战术模式三件套Entity、Value Object 与 Aggregate2.1 EntityDDD靠身份而非属性区分对象当一个对象以身份identity而非属性被区分时应把身份作为其模型定义的核心。保持类定义简洁聚焦生命周期连续性与身份。这是 Evans《领域驱动设计》对 Entity 的经典定义。判断依据很朴素是否需要在时间上持续追踪它。MyMeetings 中MeetingGroup会议小组就是典型 Entity——它从提案被接受、创建、成员加入/退出、到设置到期时间拥有完整生命周期因此必须拥有全局唯一标识Id。源码位于 src/Modules/Meetings/Domain/MeetingGroups/MeetingGroup.cspublic class MeetingGroup : Entity, IAggregateRoot { public MeetingGroupId Id { get; private set; } private string _name; private string _description; private MeetingGroupLocation _location; private MemberId _creatorId; private ListMeetingGroupMember _members; private DateTime _createDate; private DateTime? _paymentDateTo; ... }注意几个实现细节无公开 setter完全封装。所有属性都是private字段外部只能通过暴露的行为方法改变状态。Id的 setter 也是private只在构造时赋值。private无参构造函数仅用于 EFOnly for EF防止外部直接new出无效实体。internal static工厂方法如CreateBasedOnProposal作为唯一创建入口配合私有构造函数保证不变量。这正是术语卡所强调的Entity 应当fully encapsulated - you can only mutate its state via exposed behavior (no setters)。实体基类位于 src/BuildingBlocks/Domain/Entity.cs提供了AddDomainEvent与CheckRule两个受保护方法是所有实体共享的基建public abstract class Entity { private ListIDomainEvent _domainEvents; public IReadOnlyCollectionIDomainEvent DomainEvents _domainEvents?.AsReadOnly(); public void ClearDomainEvents() { _domainEvents?.Clear(); } protected void AddDomainEvent(IDomainEvent domainEvent) { _domainEvents ?? []; this._domainEvents.Add(domainEvent); } protected void CheckRule(IBusinessRule rule) { if (rule.IsBroken()) { throw new BusinessRuleValidationException(rule); } } }可以看到实体层已经内建了记录领域事件 校验业务规则的能力这与 src/BuildingBlocks/Domain/IBusinessRule.cs、src/BuildingBlocks/Domain/BusinessRuleValidationException.cs 共同构成规则校验的骨架。2.2 Value ObjectDDD不可变、无身份、按属性比较当只关心模型中某个元素的属性时把它归类为 VALUE OBJECT。使其表达属性所承载的含义并提供相关功能。将 VALUE OBJECT 视为不可变对象不赋予它身份。MyMeetings 中最典型的 Value Object 是MoneyValue金额源码位于 src/Modules/Meetings/Domain/Meetings/MoneyValue.cspublic class MoneyValue : ValueObject { public decimal Value { get; } public string Currency { get; } private MoneyValue(decimal value, string currency) { this.Value value; this.Currency currency; } public static MoneyValue Of(decimal value, string currency) { CheckRule(new ValueOfMoneyMustNotBeNegativeRule(value)); return new MoneyValue(value, currency); } // 运算符重载支持 decimal 与 MoneyValue 的比较 public static bool operator (decimal left, MoneyValue right) left right.Value; public static bool operator (decimal left, MoneyValue right) left right.Value; ... }为什么MoneyValue是 Value Object 而非 Entity不需要在时间上追踪——金额没有生命周期没有身份——我们从不问这个 100 元是谁不可变——Value与Currency只有get唯一构造入口是带规则校验的静态工厂Of按属性比较——两个MoneyValue只要Value与Currency相等就视为相等。Value Object 的相等性比较由基类 src/BuildingBlocks/Domain/ValueObject.cs 通过反射统一实现遍历类型的所有公开属性与非公开字段逐一比较值并生成哈希码同时支持/!运算符重载还可用IgnoreMemberAttribute忽略不应参与比较的成员public override bool Equals(object obj) { if (obj null || GetType() ! obj.GetType()) return false; return GetProperties().All(p PropertiesAreEqual(obj, p)) GetFields().All(f FieldsAreEqual(obj, f)); }2.3 AggregateDDD以聚合根为边界的封装与不变量保障将 ENTITY 和 VALUE OBJECT 聚类为 AGGREGATE 并为其定义边界。为每个 AGGREGATE 选择一个 ENTITY 作为根并通过根控制对边界内所有对象的访问。聚合是 DDD 战术模式中最重要也最难落地的一个。在 MyMeetings 中MeetingGroup不仅是一个 Entity更是整个聚合的聚合根Aggregate Root聚合成员MeetingGroup根、MeetingGroupLocation值对象、MeetingGroupId、MeetingGroupMember、MeetingGroupMemberRole边界控制外部只能访问根对象MeetingGroup其余成员全部私有封装不变量优先每个公开方法第一件事就是CheckRule校验业务规则。以成员加入为例MeetingGroup.cspublic void JoinToGroupMember(MemberId memberId) { this.CheckRule(new MeetingGroupMemberCannotBeAddedTwiceRule(_members, memberId)); this._members.Add(MeetingGroupMember.CreateNew(this.Id, memberId, MeetingGroupMemberRole.Member)); }MeetingGroupMemberCannotBeAddedTwiceRule先检查同一成员不能重复加入规则通过后才修改状态。这正对应 Evans 所说的 the root controls access, it cannot be blindsided by changes to the internals——聚合根控制所有访问因此不会被内部成员的意外变更所蒙蔽。再以创建会议为例CreateMeeting同时校验两条业务规则public Meeting CreateMeeting(string title, MeetingTerm term, string description, MeetingLocation location, int? attendeesLimit, int guestsLimit, Term rsvpTerm, MoneyValue eventFee, ListMemberId hostsMembersIds, MemberId creatorId) { this.CheckRule(new MeetingCanBeOrganizedOnlyByPayedGroupRule(_paymentDateTo)); this.CheckRule(new MeetingHostMustBeAMeetingGroupMemberRule(creatorId, hostsMembersIds, _members)); return Meeting.CreateNew(this.Id, title, term, description, location, MeetingLimits.Create(attendeesLimit, guestsLimit), rsvpTerm, eventFee, hostsMembersIds, creatorId); }只有已付费的小组才能组织会议MeetingCanBeOrganizedOnlyByPayedGroupRule与会议主办者必须是小组成员MeetingHostMustBeAMeetingGroupMemberRule这两条不变量在聚合根层面一次性保障。对应规则实现位于 src/Modules/Meetings/Domain/MeetingGroups/Rules/。聚合根还负责在状态变更时发出领域事件例如构造函数中AddDomainEvent(new MeetingGroupCreatedDomainEvent(this.Id, creatorId))把小组已创建这一事实记录下来。小结Entity 回答我是什么身份Value Object 回答我的属性是什么Aggregate 则回答谁能改我、改之前必须满足什么。三者共同构成了 src/BuildingBlocks/Domain/IAggregateRoot.cs 标记接口所表达的富领域模型。三、Command把意图显式化Command 是请求做某事的表达它代表系统用户关于系统将如何改变其状态的意图。Command 有三个重要特征来自 Open Agile Architecture结果只有成功或失败成功的结果是事件Event(s)成功时必然发生状态变更否则等于什么都没发生命名规范用动词现在时或不定式 来自领域的名词词组例如CancelMeeting、BuySubscription。MyMeetings 中 Command 以两种形态出现3.1 应用层 Command 对象 Handler参数对象模式以取消会议为例命令对象位于 src/Modules/Meetings/Application/Meetings/CancelMeeting/CancelMeetingCommand.cspublic class CancelMeetingCommand : CommandBase { public CancelMeetingCommand(Guid meetingId) { MeetingId meetingId; } public Guid MeetingId { get; } }命令处理器位于 src/Modules/Meetings/Application/Meetings/CancelMeeting/CancelMeetingCommandHandler.csinternal class CancelMeetingCommandHandler : ICommandHandlerCancelMeetingCommand { private readonly IMeetingRepository _meetingRepository; private readonly IMemberContext _memberContext; internal CancelMeetingCommandHandler(IMeetingRepository meetingRepository, IMemberContext memberContext) { _meetingRepository meetingRepository; _memberContext memberContext; } public async Task Handle(CancelMeetingCommand request, CancellationToken cancellationToken) { var meeting await _meetingRepository.GetByIdAsync(new MeetingId(request.MeetingId)); meeting.Cancel(_memberContext.MemberId); } }Handler 的三步曲非常清晰加载聚合 → 调用聚合行为方法 → 由 UnitOfWork 统一提交。它自身不实现任何业务逻辑业务逻辑全部封装在聚合内部。3.2 聚合上的命令方法DDD 形态同一业务在聚合上体现为行为方法src/Modules/Meetings/Domain/Meetings/Meeting.cs#L278-L290public void Cancel(MemberId cancelMemberId) { this.CheckRule(new MeetingCannotBeChangedAfterStartRule(_term)); if (!_isCanceled) { _isCanceled true; _cancelDate SystemClock.Now; _cancelMemberId cancelMemberId; this.AddDomainEvent(new MeetingCanceledDomainEvent(this.Id, _cancelMemberId, _cancelDate.Value)); } }3.3 命令可以被拒绝失败即回滚术语卡强调了一个关键点Command 在状态改变之前是可以被拒绝的。两条拒绝路径Handler 层如MeetingId无效仓储加载不到聚合直接抛异常领域层业务规则被破坏CheckRule抛出BusinessRuleValidationException。无论哪条路径命令都被拒绝所有未提交的变更随之回滚状态不变。这个全有或全无语义由基础设施层的事务装饰器保证见 src/BuildingBlocks/Infrastructure/DomainEventsDispatching/UnitOfWorkCommandHandlerDecorator.cspublic async Task Handle(T command, CancellationToken cancellationToken) { await this._decorated.Handle(command, cancellationToken); await this._unitOfWork.CommitAsync(cancellationToken); }Handler 执行成功后统一CommitAsync任何异常都不会进入提交阶段。3.4 Command 的两种边界用户命令与内部命令从源码结构看Meetings 模块还区分了外部命令与内部命令两种载体。内部命令InternalCommandBase位于 src/Modules/Meetings/Application/Configuration/Commands/InternalCommandBase.cs用于异步处理流程如发邮件、订阅到期检查等配合 Outbox / 定时调度机制消费是模块内部实现命令队列的基建。四、Domain Event 与 Integration Event两种事件的分工4.1 Event 的定义与分类事件是发生在过去的事情。这是最简洁也最根本的定义。因为发生在过去事件天然不可变、只能追加。在 MyMeetings 中事件分为两类Domain Event领域事件发生在领域内、需要同一进程内其他部分感知的事件Integration Event集成事件用于模块之间跨进程/跨边界异步通信的事件。4.2 Domain Event进程内的广播以购买订阅为例SubscriptionPayment聚合根在创建支付记录时发出SubscriptionPaymentCreatedDomainEvent位于 src/Modules/Payments/Domain/SubscriptionPayments/Events/ 对应文件public class SubscriptionPaymentCreatedDomainEvent : DomainEventBase { public Guid SubscriptionPaymentId { get; } public Guid PayerId { get; } public string SubscriptionPeriodCode { get; } public string CountryCode { get; } public string Status { get; } public decimal Value { get; } public string Currency { get; } ... }事件基类 src/BuildingBlocks/Domain/DomainEventBase.cs 提供通用字段public class DomainEventBase : IDomainEvent { public Guid Id { get; } public DateTime OccurredOn { get; } public DomainEventBase() { this.Id Guid.NewGuid(); this.OccurredOn DateTime.UtcNow; } }Id事件自身的自动生成唯一标识OccurredOn事件发生的时刻UTC。领域事件的所有属性都是getonly——事件是过去的事实你无法改变过去。领域事件的分发由 src/BuildingBlocks/Infrastructure/DomainEventsDispatching/DomainEventsDispatcher.cs 统一负责在每次命令提交前从聚合根收集DomainEvents再通过内存事件总线分发给进程内的处理器notification handler。这正是 ADR0014-event-driven-communication-between-modules.md与0015-use-in-memory-events-bus.md的落地。4.3 Integration Event模块间通信的契约领域事件不出模块边界跨模块必须走集成事件。例如 src/Modules/Meetings/IntegrationEvents/MeetingGroupProposedIntegrationEvent.cs、src/Modules/Meetings/IntegrationEvents/MemberCreatedIntegrationEvent.cs 等均定义在独立的IntegrationEvents程序集中供其他模块引用。典型的跨模块流程Meetings 模块领域层发出MeetingGroupProposedDomainEvent基础设施层将其转换为MeetingGroupProposedIntegrationEvent映射逻辑见 src/BuildingBlocks/Infrastructure/DomainEventsDispatching/DomainNotificationsMapper.cs通过 Outbox发件箱持久化并异步投递Administration 模块的MeetingGroupProposedIntegrationEventHandlersrc/Modules/Administration/Application/MeetingGroupProposals/MeetingGroupProposedIntegrationEventHandler.cs消费该事件创建对应的待审核提案。这套机制保证了模块间的最终一致性发送方与接收方各自在本地事务中完成写入事件通过 src/BuildingBlocks/Infrastructure/EventBus/InMemoryEventBus.cs 与 Outbox 异步传递。五、行为型与结构型模式Decorator、Strategy、Dependency Injection5.1 Dependency Injection依赖由外部注入而非自行创建依赖注入是一种技术对象接收它所依赖的其他对象这些对象被称为依赖。CancelMeetingCommandHandler需要两个协作者会议仓储IMeetingRepository和成员上下文IMemberContext。它不自己new实现而是通过构造函数注入接收接口internal CancelMeetingCommandHandler(IMeetingRepository meetingRepository, IMemberContext memberContext) { _meetingRepository meetingRepository; _memberContext memberContext; }这样带来的好处是Handler 依赖的是抽象接口而非具体实现测试时可以轻松替换为 Mock/Stub。这正是 src/Modules/Meetings/Application/Meetings/CancelMeeting/CancelMeetingCommandHandler.cs 的实际写法。同时仓库为每个模块建立独立 IoC 容器ADR0016-create-ioc-container-per-module.md模块间不共享容器进一步强化模块边界。5.2 Decorator Pattern不改皮肤逻辑动态叠加横切关注点装饰器模式允许动态地为单个对象添加行为而不影响同类的其他对象。它常与单一职责原则配合将功能按关注点拆分为多个类。术语卡用LoggingCommandHandlerDecorator作为示例。这个类在仓库五个模块中均有同名实现例如 src/Modules/Meetings/Infrastructure/Configuration/Processing/LoggingCommandHandlerDecorator.csinternal class LoggingCommandHandlerDecoratorT : ICommandHandlerT where T : ICommand { private readonly ILogger _logger; private readonly IExecutionContextAccessor _executionContextAccessor; private readonly ICommandHandlerT _decorated; public async Task Handle(T command, CancellationToken cancellationToken) { if (command is IRecurringCommand) { return await _decorated.Handle(command, cancellationToken); } using (LogContext.Push( new RequestLogEnricher(_executionContextAccessor), new CommandLogEnricher(command))) { try { this._logger.Information(Executing command {Command}, command.GetType().Name); var result await _decorated.Handle(command, cancellationToken); this._logger.Information(Command {Command} processed successful, command.GetType().Name); return result; } catch (Exception exception) { this._logger.Error(exception, Command {Command} processing failed, command.GetType().Name); throw; } } } }装饰器的独特之处在于它身兼两职实现ICommandHandlerT即component角色同时接受另一个ICommandHandlerT实现即concrete component角色通常通过依赖注入传入。从示例可以看到装饰器的典型价值IRecurringCommand定时/内部命令直接透传不做日志上下文包装普通命令在执行前后记录开始/成功/失败日志CommandLogEnricher把命令 Id 写入日志上下文RequestLogEnricher把请求的CorrelationId由 src/API/CompanyName.MyMeetings.API/Configuration/ExecutionContext/CorrelationMiddleware.cs 产生写入日志上下文。这样命令处理链内任何一层产生的日志都自动携带命令 Id 与关联请求 Id排查问题时可一键串联。在 MyMeetings 中命令处理管道是典型的装饰器链LoggingCommandHandlerDecorator→UnitOfWorkCommandHandlerDecorator→DomainEventsDispatcherNotificationHandlerDecorator等依次包裹真实 Handler。每个装饰器只负责一个横切关注点日志、事务、事件分发互不干扰正体现把功能按关注点拆分的设计思想。注意Decorator 极易与 Strategy 混淆。一句话区分——装饰器改变对象的皮肤外层行为策略改变对象的内脏内部算法。5.3 Strategy Pattern运行时选择算法策略模式也称政策模式是行为型设计模式允许在运行时选择算法代码不直接实现单一算法而是接收运行时的指令来决定使用算法族中的哪一个。策略模式有四个参与者Client客户端调用方代码Context上下文持有具体策略引用、与客户端交互的对象Strategy interface策略接口客户端通过 Context 在运行时设置具体策略所用的接口Concrete strategies具体策略策略接口的一个或多个实现。MyMeetings 的定价子系统是策略模式的教科书式应用BuySubscriptionCommandHandler客户端通过PriceListFactory间接为PriceList设置当前策略PriceList上下文持有IPricingStrategy引用IPricingStrategy策略接口src/Modules/Payments/Domain/PriceListItems/PricingStrategies/IPricingStrategy.cspublic interface IPricingStrategy { MoneyValue GetPrice(string countryCode, SubscriptionPeriod subscriptionPeriod, PriceListItemCategory category); }三个具体策略DirectValueFromPriceListPricingStrategy直接返回价目表价格默认策略DiscountedValueFromPriceListPricingStrategy在价目表价格上减去折扣额DirectValuePricingStrategy直接返回固定值。三者均位于 src/Modules/Payments/Domain/PriceListItems/PricingStrategies/。工厂中目前默认选用直接取价目表价格// 这是根据提供的数据与系统状态选择定价策略的地方。 IPricingStrategy pricingStrategy new DirectValueFromPriceListPricingStrategy(priceListItems); return PriceList.Create(priceListItems, pricingStrategy);来源src/Modules/Payments/Application/PriceListItems/PriceListFactory.cs。PriceList的GetPrice在策略执行前先校验该国家、周期、类别的价格必须已定义PriceForSubscriptionMustBeDefinedRule保证策略算法基于合法数据运行。一句话区分策略让你改变对象的内脏装饰器让你改变皮肤。购买订阅这个用例同时也是多个模式组合的范例——Factory 负责创建PriceListStrategy 负责定价算法。六、从术语到测试这些概念如何被验证术语表不只是名词解释MyMeetings 为这些模式提供了对应的测试验证单元测试Meetings 模块在 src/Modules/Meetings/Tests/UnitTests/ 下针对MeetingGroup、Meeting等领域对象编写了大量测试覆盖聚合规则如只有付费小组才能组织会议架构测试src/Tests/ArchTests/与各模块的ArchTests项目如 src/Modules/Meetings/Tests/ArchTests/用自动化测试守护模块边界防止跨模块非法引用集成测试src/Modules/Meetings/Tests/IntegrationTests/验证跨模块事件流程如小组提案被接受后创建会议小组的真实数据库行为。术语卡中出现的 Act/Arrange/Assert、Given When Then、Mock、Stub、Integration Test、Unit Test 等条目都可以在这三个测试层次中找到对应实现。这也解释了为什么术语表会把测试相关术语与 DDD 术语并列——模式的可信度来自测试的覆盖。七、尚待填充的术语与阅读建议值得注意的是术语表中部分条目目前只有标题TODO 状态包括Event-Driven Architecturedocs/catalog-of-terms/Event-Driven-Architecture/README.mdEvent Sourcingdocs/catalog-of-terms/Event-Sourcing/README.mdEvent Stormingdocs/catalog-of-terms/Event-Storming/README.mdIntegration Eventdocs/catalog-of-terms/Integration-Event/README.md阅读这些主题时可以借助仓库中的其他资料补齐上下文Event Sourcing 的 Payload 结构、事件表设计与投影机制可参考 docs/Images/ES_event_store_db_sample.png 与 docs/Images/ES_events_projection.png以及 Payments 模块的AggregateStoresrc/Modules/Payments/Infrastructure/AggregateStore/Event Storming 的工作坊产物可参考 docs/Images/Payments_EventStorming_Design.jpg、docs/Images/User_Registration.jpg 等设计稿集成事件的实际用法可参考 src/Modules/Meetings/IntegrationEvents/ 与各模块的*IntegrationEventHandler.cs消费端。八、总结一份术语表如何撑起整套架构回到docs/catalog-of-terms/README.md本身这份索引的价值在于它把散落在整个代码库中的设计决策浓缩为一套统一的领域语言层次关键术语仓库证据战术模式Aggregate、Entity、Value ObjectMeetingGroup.cs、MoneyValue.cs应用层Command、CQRS、Query、Read ModelCancelMeetingCommand.cs事件Domain Event、Integration Event、Eventual ConsistencyDomainEventsDispatcher.cs、InMemoryEventBus.cs模式Decorator、Strategy、Dependency Injection、FactoryLoggingCommandHandlerDecorator.cs、PriceListFactory.cs工程化ADR、Architecture Test、Integration Test、CIdocs/architecture-decision-log/、src/Tests/ArchTests/对于想要落地 DDD 的团队这份术语表 源码的双重学习路径极具参考价值先读术语卡片理解模式意图再对照源码看真实实现最后通过测试用例验证行为。以这样的方式学习术语不再是抽象名词而是可运行、可测试、可复用的工程实践。【免费下载链接】modular-monolith-with-dddFull Modular Monolith application with Domain-Driven Design approach.项目地址: https://gitcode.com/GitHub_Trending/mo/modular-monolith-with-ddd创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考