本文书接上回《一种很变态但有效的DDD建模沟通方式》,关注公众号(老肖想当外语大佬)获取信息:
-
最新文章更新;
-
DDD框架源码(.NET、Java双平台);
-
加群畅聊,建模分析、技术交流;
-
视频和直播在B站。
终于到了写代码的环节
如果你已经阅读过本系列前面的所有文章,我相信你对需求分析和建模设计有了更深刻的理解,那么就可以实现“需求-模型-代码”三者一致性的前半部分,如下图所示:

那么接下来,我们来分析一下如何实现“模型-代码”的一致性,尝试通过一篇文章的篇幅,展示符合DDD价值判断的代码组织方式的关键部分,初步窥探一下DDD实践的代码样貌:

领域模型与充血模型
现在假设我们通过需求分析,完成了对模型的设计,并推演确认模型满足提出的所有需求,既然模型满足需求,那么意味着我们设计的模型具备下面特征:
-
每个模型有自己明确的职责,这些职责分别对应这着不同的需求点;
-
每个模型都包含自己履行职责所需要的所有属性信息;
-
每个模型都包含履行职责行为能力,并可以发出对应行为产生的事件;
那么提炼下来,我们会发现模型必须是“充血模型”,即同时包含属性和行为,模型与代码的对应关系如下:

我们可以类图来表达模型,即一个聚合根,也可以称之为一个领域,当然一个聚合根可以包含一些复杂类型属性或集合属性,下图示意了一个简单的用户聚合:

下面展示了该模型的示例代码:
Java代码:
package com.yourcompany.domain.aggregates; import com.yourcompany.domain.aggregates.events.*; import lombok.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; import org.hibernate.annotations.Fetch; import org.hibernate.annotations.FetchMode; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import org.netcorepal.cap4j.ddd.domain.event.annotation.DomainEvent; import org.netcorepal.cap4j.ddd.domain.event.impl.DefaultDomainEventSupervisor; import javax.persistence.*; /** * 用户 * <p> * 本文件由[cap4j-ddd-codegen-maven-plugin]生成 * 警告:请勿手工修改该文件的字段声明,重新生成会覆盖字段声明 */ /* @AggregateRoot */ @Entity @Table(name = "`user`") @DynamicInsert @DynamicUpdate @AllArgsConstructor @NoArgsConstructor @Builder @Getter public class User { // 【行为方法开始】 public void init() { DefaultDomainEventSupervisor.instance.attach(UserCreatedDomainEvent.builder() .user(this) .build(), this); } public void changeEmail(String email) { this.email = email; DefaultDomainEventSupervisor.instance.attach(UserEmailChangedDomainEvent.builder() .user(this) .build(), this); } // 【行为方法结束】 // 【字段映射开始】本段落由[cap4j-ddd-codegen-maven-plugin]维护,请不要手工改动 @Id @GeneratedValue(generator = "org.netcorepal.cap4j.ddd.application.distributed.SnowflakeIdentifierGenerator") @GenericGenerator(name = "org.netcorepal.cap4j.ddd.application.distributed.SnowflakeIdentifierGenerator", strategy = "org.netcorepal.cap4j.ddd.application.distributed.SnowflakeIdentifierGenerator") @Column(name = "`id`") Long id; /** * varchar(100) */ @Column(name = "`name`") String name; /** * varchar(100) */ @Column(name = "`email`") String email; // 【字段映射结束】本段落由[cap4j-ddd-codegen-maven-plugin]维护,请不要手工改动 }
C#代码:

领域事件的定义如下:
Java代码:
package com.yourcompany.domain.aggregates.events; import com.yourcompany.domain.aggregates.User; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.netcorepal.cap4j.ddd.domain.event.annotation.DomainEvent; /** * 用户创建事件 */ @DomainEvent @Data @AllArgsConstructor @NoArgsConstructor @Builder public class UserCreatedDomainEvent { User user; } package com.yourcompany.domain.aggregates.events; import com.yourcompany.domain.aggregates.User; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.netcorepal.cap4j.ddd.domain.event.annotation.DomainEvent; /** * 用户邮箱变更事件 */ @DomainEvent @Data @AllArgsConstructor @NoArgsConstructor @Builder public class UserEmailChangedDomainEvent { User user; }
C#代码:
//定义领域事件 using NetCorePal.Extensions.Domain; namespace YourNamespace; public record UserCreatedDomainEvent(User user) : IDomainEvent; public record UserEmailChangedDomainEvent(User user) : IDomainEvent;
至此,我们的一个领域模型的代码就完成了。
领域模型之外的关键要素
让我们再回到“模型拟人化”的类比上,想象一下在企业里一个任务是怎么被完成的,下图展示了一个典型流程:

如果我们将这个过程对应到软件系统,可以得到如下流程:

根据上面的对应我可以知道除了领域模型之外,其他的关键要素:
-
Controller
-
Command与CommandHandler
-
DomainEventHandler
接下来,我们分别对这些部分进行说明
Controller
有过web项目开发经验的开发者,对Controller并不陌生,它是web服务与前端交互的入口,在这里Controller的主要职责是:
-
接收外部输入
-
将请求输入及当前用户会话等信息组装成命令
-
发出/执行命令
-
响应命令执行结果
Java代码:
package com.yourcompany.adapter.portal.api; import com.yourcompany.adapter.portal.api._share.ResponseData; import com.yourcompany.application.commands.CreateUserCommand; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; /** * 用户控制器 */ @Tag(name = "用户") @RestController @RequestMapping(value = "/api/user") @Slf4j public class UserController { @Autowired CreateUserCommand.Handler createUserCommandHandler; @PostMapping("/") public ResponseData<Long> createUserCommand(@RequestBody @Valid CreateUserCommand cmd) { Long result = createUserCommandHandler.exec(cmd); return ResponseData.success(result); } }
C#代码:
[Route("api/[controller]")] [ApiController] public class UserController(IMediator mediator) : ControllerBase { [HttpPost] public async Task<ResponseData<UserId>> Post([FromBody] CreateUserRequest request) { var cmd = new CreateUserCommand(request.Name, request.Email); var id = await mediator.Send(cmd); return id.AsResponseData(); } }
===
===
Command与CommandHandler
基于前面的对应关系,Command对应任务,那么我们可以这样理解:
-
Command是执行任务所需要的信息
-
CommandHandler负责将命令信息传递给领域模型
-
CommandHandler最后要将领域模型持久化
下面是一个简单的示例:
Java代码:
package com.yourcompany.application.commands; import com.yourcompany.domain.aggregates.User; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.netcorepal.cap4j.ddd.application.command.Command; import org.netcorepal.cap4j.ddd.domain.repo.AggregateRepository; import org.netcorepal.cap4j.ddd.domain.repo.UnitOfWork; import org.springframework.stereotype.Service; /** * 创建用户命令 */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CreateUserCommand { String name; String email; @Service @RequiredArgsConstructor @Slf4j public static class Handler implements Command<CreateUserCommand, Long> { private final AggregateRepository<User, Long> repo; private final UnitOfWork unitOfWork; @Override public Long exec(CreateUserCommand cmd) { User user = User.builder() .name(cmd.name) .email(cmd.email) .build(); user.init(); unitOfWork.persist(user); unitOfWork.save(); return user.getId(); } } }
C#代码:
public record CreateUserCommand(string Name, string Email) : ICommand<UserId>; public class CreateUserCommandHandler(IUserRepository userRepository) : ICommandHandler<CreateUserCommand, UserId> { public async Task<UserId> Handle(CreateUserCommand request, CancellationToken cancellationToken) { var user = new User(request.Name, request.Email); user = await userRepository.AddAsync(user, cancellationToken); return user.Id; } }
===
===
DomainEventHandler
当我们的命令执行完成,领域模型会产生领域事件,那么关心领域事件,期望在领域事件发生时执行一些操作,就可以使用DomainEventHandler来完成:
-
DomainEventHandler根据事件信息产生新的命令并发出
-
每个DomainEventHandler只做一件事,即只发出一个命令
Java代码:
package com.yourcompany.application.subscribers; import com.yourcompany.application.commands.DoSomethingCommand; import com.yourcompany.domain.aggregates.events.UserCreatedDomainEvent; import lombok.RequiredArgsConstructor; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; /** * 用户创建领域事件 */ @Service @RequiredArgsConstructor public class UserCreatedDomainEventHandler { private final DoSomethingCommand.Handler handler; @EventListener(UserCreatedDomainEvent.class) public void handle(UserCreatedDomainEvent event) { handler.exec(DoSomethingCommand.builder() .param(event.getUser().getId()) .build()); } }
C#代码:
public class UserCreatedDomainEventHandler(IMediator mediator) : IDomainEventHandler<UserCreatedDomainEvent> { public Task Handle(UserCreatedDomainEvent notification, CancellationToken cancellationToken) { return mediator.Send(new DoSomethingCommand(notification.User.Id), cancellationToken); } }
===
===
模型的持久化
在前文,我们一直强调一个观点,“在设计模型时忘掉数据库”,那么当我们完成模型设计之后,如何将模型存储进数据库呢?通常我们会使用仓储模式在负责模型的“存取”操作,下面代码示意了一个仓储具备的基本能力以及仓储的定义,略微不同的是,我们实现了工作单元模式(UnitOfWork),以屏蔽数据库的“增删改查”语义,我们只需要从仓储中“取出模型”、“操作模型”、“保存模型”即可。
Java代码:
package com.yourcompany.adapter.domain.repositories; import com.yourcompany.domain.aggregates.User; /** * 本文件由[cap4j-ddd-codegen-maven-plugin]生成 */ public interface UserRepository extends org.netcorepal.cap4j.ddd.domain.repo.AggregateRepository<User, Long> { // 【自定义代码开始】本段落之外代码由[cap4j-ddd-codegen-maven-plugin]维护,请不要手工改动 @org.springframework.stereotype.Component public static class UserJpaRepositoryAdapter extends org.netcorepal.cap4j.ddd.domain.repo.AbstractJpaRepository<User, Long> { public UserJpaRepositoryAdapter(org.springframework.data.jpa.repository.JpaSpecificationExecutor<User> jpaSpecificationExecutor, org.springframework.data.jpa.repository.JpaRepository<User, Long> jpaRepository) { super(jpaSpecificationExecutor, jpaRepository); } } // 【自定义代码结束】本段落之外代码由[cap4j-ddd-codegen-maven-plugin]维护,请不要手工改动 }
C#代码:
public interface IRepository<TEntity, TKey> : IRepository<TEntity> where TEntity : notnull, Entity<TKey>, IAggregateRoot where TKey : notnull { IUnitOfWork UnitOfWork { get; } TEntity Add(TEntity entity); Task<TEntity> AddAsync(TEntity entity, CancellationToken cancellationToken = default (CancellationToken)); int DeleteById(TKey id); Task<int> DeleteByIdAsync(TKey id, CancellationToken cancellationToken = default (CancellationToken)); TEntity? Get(TKey id); Task<TEntity?> GetAsync(TKey id, CancellationToken cancellationToken = default (CancellationToken)); } public interface IUserRepository : IRepository<User, UserId> { } public class UserRepository(ApplicationDbContext context) : RepositoryBase<User, UserId, ApplicationDbContext>(context), IUserRepository { }
===
===
查询的处理
下面展示了一个简单的查询的代码
Java代码:
package com.yourcompany.application.queries; import com.yourcompany._share.exception.KnownException; import com.yourcompany.domain.aggregates.User; import com.yourcompany.domain.aggregates.schemas.UserSchema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.netcorepal.cap4j.ddd.application.query.Query; import org.netcorepal.cap4j.ddd.domain.repo.AggregateRepository; import org.springframework.stereotype.Service; /** * 查询用户 */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class UserQuery { private Long id; @Service @RequiredArgsConstructor @Slf4j public static class Handler implements Query<UserQuery, UserQueryDto> { private final AggregateRepository<User, Long> repo; @Override public UserQueryDto exec(UserQuery param) { User entity = repo.findOne(UserSchema.specify( root -> root.id().eq(param.id) )).orElseThrow(() -> new KnownException("不存在")); return UserQueryDto.builder() .id(entity.getId()) .name(entity.getName()) .email(entity.getEmail()) .build(); } } @Data @AllArgsConstructor @NoArgsConstructor @Builder public static class UserQueryDto { private Long id; private String name; private String email; } }
C#代码:
public class UserQuery(ApplicationDbContext applicationDbContext) { public async Task<UserDto?> QueryOrder(UserId userId, CancellationToken cancellationToken) { return await applicationDbContext.Users.Where(p => p.Id == userId) .Select(p => new UserDto(p.Id, p.Name)).SingleOrDefault(); } }
===
===
CQRS似乎是唯一正解
我们在实际的软件系统中,查询往往是场景复杂的,不同的查询需求,可能打破模型的整体性,显然使用领域模型本身来满足这些需求是不现实的,那么就需要针对需求场景,组织对应的数据结构作为输出结果,这就与“CQRS”模式不谋而合,或者说“CQRS”就是为了解决这个问题而被提出的,并且这个模式与“命令-事件”的思维浑然一体,前面的代码示例也印证了这一点,因此我们认为DDD的实践落地,需要借助CQRS的模式。

源码资料
本文示例分别使用了cap4j(Java)和netcorepal-cloud-framework(dotnet),欢迎参与项目讨论和贡献,项目地址如下: