- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
本文书接上回《一种很变态但有效的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 。
接下来,我们分别对这些部分进行说明 。
有过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对应任务,那么我们可以这样理解:
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只做一件事,即只发出一个命令 。
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”就是为了解决这个问题而被提出的,并且这个模式与“命令-事件”的思维浑然一体,前面的代码示例也印证了这一点,因此我们认为DDD的实践落地,需要借助CQRS的模式.
本文示例分别使用了cap4j(Java)和netcorepal-cloud-framework(dotnet),欢迎参与项目讨论和贡献,项目地址如下:
https://github.com/netcorepal/cap4j 。
https://github.com/netcorepal/netcorepal-cloud-framework 。
最后此篇关于DDD建模后写代码的正确姿势(Java、dotnet双平台)的文章就讲到这里了,如果你想了解更多关于DDD建模后写代码的正确姿势(Java、dotnet双平台)的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我需要(我必须)将大量 float 写入 qdatastream 并且我只使用 4 个字节是必要的。setFloatingPointPrecision 或为 float 和 double 写入 4 或
我有一些 C 代码,我用 Python 对其进行了扩展。扩展的 C 代码有一个将一些结构附加到二进制文件的函数: void writefunction(const struct struct1* so
我正在用 C 语言开发一个小软件,用于在布告栏中读取和写入消息。每条消息都是一个以渐进数字命名的 .txt。 软件是多线程的,有很多用户可以并发操作。 用户可以进行的操作有: 阅读整个公告板(所有 .
我有 2 个线程同时访问同一个大文件 (.txt)。 第一个线程正在从文件中读取。第二个线程正在写入文件。 两个线程都访问同一个 block ,例如(开始:0, block 大小:10),但具有不同的
我做了很多谷歌搜索,但我仍然不确定如何继续。 Linux 下最常见的剪贴板读写方式是什么?我想要同时支持 Gnome 和 KDE 桌面。 更新:我是否认为没有简单的解决方案,必须将多个来源(gnome
1. 定义配置文件信息 有时候我们为了统一管理会把一些变量放到 yml 配置文件中 例如 图片 用 @ConfigurationProperties 代替 @Value 使用方法 定义对应字段的实体
在开始之前,我必须先声明我是 FORTRAN 的新手。我正在维护 1978 年的一段遗留代码。它的目的是从文件中读取一些数据值,处理这些值,然后将处理过的值输出到另一个文本文件。 给定以下 FORTR
我正在制作一个应用程序,我需要存储用户提供的一些信息。我尝试使用 .plist 文件来存储信息,我发现: NSString *filePath = @"/Users/Denis/Documents/X
在delphi类中声明属性时是否可能有不同类型的结果? 示例: 属性月份:字符串读取monthGet(字符串)写入monthSet(整数); 在示例中,我希望在属性(property)月份中,当我:读
我正在以二进制形式将文件加载到数组中,这似乎需要一段时间有没有更好更快更有效的方法来做到这一点。我正在使用类似的方法写回文件。 procedure openfile(fname:string); va
我想实现一个运行模拟的C#控制台应用程序。另外,我想给用户机会在控制台上按“+”或“-”来加速/减速模拟的速度。 有没有办法在编写控制台时读取控制台?我相信我可以为此使用多线程,但是我却不怎么做(我对
这是我的代码: use std::fs::File; use std::io::Write; fn main() { let f = File::create("").unwrap();
我有一个应用程序可以访问 csv 文本文件中的单词。由于它们通常不会更改,因此我将它们放置在 .jar 文件中,并使用 .getResourceAsStream 调用读取它们。我真的很喜欢这种方法,因
我使用kubeadm,docker 17.12.1-ce和法兰绒网络安装了Kubernetes 1.13.1集群 但是,我发现Kubernetes主服务器上有许多空文件,权限为666,该文件允许任何用
我的工作区中有一些 java 文件。现在我想编写一个java程序,它可以读取来自不同源的文本文件,一次一个,一行一行,并将这些行插入到工作区中各自的java文件中。 文本文件会告诉我将哪个文件插入到哪
用户A要求系统读取文件foo,同时用户B想要将他或她的数据保存到同一个文件中。在文件系统级别如何处理这种情况? 最佳答案 大多数文件系统(但不是全部)使用锁定来保护对同一文件的并发访问。锁可以是独占的
我对保护移动应用程序的 firebase 数据库有一些疑问。 例如,在反编译Android应用程序后,黑客可以获取firebase api key 然后访问firebase数据库,这是正确的吗? 假设
我想让文件从外部不可删除,并希望使用java从程序对该文件进行读/写操作。 S0,我使用以下代码使用java创建了不可删除的文件: Process pcs = Runtime.getRunti
当 Selector.select() 以阻塞模式等待读/写操作时,是否可以将写消息推送到客户端?如何将选择器从阻塞模式移至写入模式?触发器可以是一个后台线程,用于放置需要写入给定 channel 的
我目前正在学习在 Linux 环境中使用 C 进行套接字编程。作为一个项目,我正在尝试编写一个基本的聊天服务器和客户端。 目的是让服务器为每个连接的客户端派生一个进程。 我遇到的问题是读取一个 chi
我是一名优秀的程序员,十分优秀!