首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

DDD建模后写代码的正确姿势(Java、dotnet双平台)

编程知识
2024年08月22日 07:09

本文书接上回《一种很变态但有效的DDD建模沟通方式》,关注公众号(老肖想当外语大佬)获取信息:

  1. 最新文章更新;

  2. DDD框架源码(.NET、Java双平台);

  3. 加群畅聊,建模分析、技术交流;

  4. 视频和直播在B站。

终于到了写代码的环节

如果你已经阅读过本系列前面的所有文章,我相信你对需求分析和建模设计有了更深刻的理解,那么就可以实现“需求-模型-代码”三者一致性的前半部分,如下图所示:

图片

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

图片

领域模型与充血模型

现在假设我们通过需求分析,完成了对模型的设计,并推演确认模型满足提出的所有需求,既然模型满足需求,那么意味着我们设计的模型具备下面特征:

  1. 每个模型有自己明确的职责,这些职责分别对应这着不同的需求点;

  2. 每个模型都包含自己履行职责所需要的所有属性信息;

  3. 每个模型都包含履行职责行为能力,并可以发出对应行为产生的事件;

那么提炼下来,我们会发现模型必须是“充血模型”,即同时包含属性和行为,模型与代码的对应关系如下:

图片

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

图片

下面展示了该模型的示例代码:

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;

至此,我们的一个领域模型的代码就完成了。

领域模型之外的关键要素

让我们再回到“模型拟人化”的类比上,想象一下在企业里一个任务是怎么被完成的,下图展示了一个典型流程:

图片

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

图片

根据上面的对应我可以知道除了领域模型之外,其他的关键要素:

  1. Controller

  2. Command与CommandHandler

  3. DomainEventHandler

接下来,我们分别对这些部分进行说明

Controller

有过web项目开发经验的开发者,对Controller并不陌生,它是web服务与前端交互的入口,在这里Controller的主要职责是:

  1. 接收外部输入

  2. 将请求输入及当前用户会话等信息组装成命令

  3. 发出/执行命令

  4. 响应命令执行结果

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对应任务,那么我们可以这样理解:

  1. Command是执行任务所需要的信息

  2. CommandHandler负责将命令信息传递给领域模型

  3. 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来完成:

  1. DomainEventHandler根据事件信息产生新的命令并发出

  2. 每个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),欢迎参与项目讨论和贡献,项目地址如下:

https://github.com/netcorepal/cap4j

https://github.com/netcorepal/netcorepal-cloud-framework

From:https://www.cnblogs.com/xiaoweiyu/p/18372863
本文地址: http://www.shuzixingkong.net/article/1316
0评论
提交 加载更多评论
其他文章 使用分布式锁解决IM聊天数据重复插入的问题
导航 业务背景 问题分析与定位 探索可行的解决方案 数据库层面处理——唯一索引 应用程序层面处理——分布式锁 分布式锁概述 分布式锁需要具备哪些特性? 分布式锁有哪些实现方式? 基于数据库的实现方式 基于Redisson实现方式 Redission介绍 概述 可重入锁 基于Redisson解决方案
使用分布式锁解决IM聊天数据重复插入的问题 使用分布式锁解决IM聊天数据重复插入的问题 使用分布式锁解决IM聊天数据重复插入的问题
Dapr v1.14 版本已发布
Dapr是一套开源、可移植的事件驱动型运行时,允许开发人员轻松立足云端与边缘位置运行弹性、微服务、无状态以及有状态等应用程序类型。Dapr能够确保开发人员专注于编写业务逻辑,而不必分神于解决分布式系统难题,由此显著提高生产力并缩短开发时长。Dapr 是用于构建云原生应用程序的开发人员框架,可以更轻松
使用FModel提取黑神话悟空的资产
目录前言设置效果展示闲聊可能遇到的问题没有相应的UE引擎版本选项 前言 黑神话悟空昨天上线了,解个包looklook。 本文内容比较简洁,仅介绍解包黑神话所需的专项配置,关于FModel的基础使用流程,请见《使用FModel提取UE4/5游戏资产》 本文仅演示steam平台下的解包过程 设置 在FM
使用FModel提取黑神话悟空的资产 使用FModel提取黑神话悟空的资产 使用FModel提取黑神话悟空的资产
Python被远程主机强制关闭后怎么自动重新运行进程
要实现Python程序在被远程主机强制关闭后能够自动重新运行,我们可以采用几种方法,但最直接且常用的方法之一是结合操作系统级的工具或脚本。在Linux系统中,我们可以使用cron作业或者systemd服务来实现这一功能;在Windows系统中,可以使用任务计划程序。但在这里,为了提供一个跨平台的、更
从源码分析 SpringBoot 的 LoggingSystem → 它是如何绑定日志组件的
开心一刻 今天心情不好,想约哥们喝点 我:心情不好,给你女朋友说一声,来我家,过来喝点 哥们:行!我给她说一声 我:你想吃啥?我点外卖 哥们:你俩定吧,我已经让她过去了 我:???我踏马让你过来!和她说一声 哥们:哈哈哈,我踏马寻思让她过去呢 前情回顾 SpringBoot2.7 霸王硬上弓 Log
从源码分析 SpringBoot 的 LoggingSystem → 它是如何绑定日志组件的 从源码分析 SpringBoot 的 LoggingSystem → 它是如何绑定日志组件的 从源码分析 SpringBoot 的 LoggingSystem → 它是如何绑定日志组件的
一个能够生成 Markdown 表格的 Bash 脚本
哈喽大家好,我是咸鱼。 今天分享一个很实用的 bash 脚本,可以通过手动提供单元格内容和列数或者将带有分隔符的文件(如 CSV、TSV 文件)转换为 Markdown 表格。 源代码在文末哦!原文链接:https://josh.fail/2022/pure-bash-markdown-table-
聊聊 PHP 多进程模式下的孤儿进程和僵尸进程
在 PHP 的编程实践中多进程通常都是在 cli 脚本的模式下使用,我依稀还记得在多年以前为了实现从数据库导出千万级别的数据,第一次在 PHP 脚本中采用了多进程编程。
聊聊 PHP 多进程模式下的孤儿进程和僵尸进程 聊聊 PHP 多进程模式下的孤儿进程和僵尸进程
零基础学习人工智能—Python—Pytorch学习(七)
前言 本文主要讲神经网络的下半部分。 其实就是结合之前学习的全部内容,进行一次神经网络的训练。 神经网络 下面是使用MNIST数据集进行的手写数字识别的神经网络训练和使用。 MNIST 数据集,是一个常用的手写数字识别数据集。MNIST 数据集包含 60,000 张 28x28 像素的灰度训练图像和
零基础学习人工智能—Python—Pytorch学习(七) 零基础学习人工智能—Python—Pytorch学习(七) 零基础学习人工智能—Python—Pytorch学习(七)