Location>code7788 >text

The right posture for writing code after DDD modeling (Java, dotnet dual platform)

Popularity:842 ℃/2024-08-22 08:17:22

This instrument follows on from the previousA very sick but effective way to communicate DDD modeling", follow the public number (Old Shaw wants to be a foreign language big brother) for information:

  1. Latest Article Updates;

  2. DDD framework source code (.NET, Java dual platform);

  3. Add a group to chat, modeling and analysis, technical exchange;

  4. Video and live streaming on B-site.

Finally, it's time to write the code

If you have read all the previous articles in this series, I believe you have a better understanding of requirements analysis and modeling design, then you can achieve the first half of the "Requirements-Model-Code" triad of consistency, as shown in the following figure:

图片

Then, let's analyze how to achieve the consistency of "model-code", and try to show the key parts of the code organization in line with the value judgment of DDD through the length of an article, so as to have a preliminary glimpse of the code of DDD practice:

图片

Domain Model and Congestion Model

Now suppose we have completed the design of the model through requirements analysis and deduced to confirm that the model satisfies all the requirements presented, since the model satisfies the requirements, it means that the model we have designed has the following characteristics:

  1. Each model has its own well-defined responsibilities, which correspond to different requirements;

  2. Each model contains all the attribute information it needs to fulfill its responsibilities;

  3. Each model contains the ability to perform duty behaviors and can emit the events generated by the corresponding behavior;

Distilled down, then, we find that the model must be "congestion model", i.e., it contains both attributes and behaviors, and the correspondence between the model and the code is as follows:

图片

We can express the model as a class diagram, i.e., an aggregate root, which can also be called a domain, and of course an aggregate root can contain some complex type attributes or set attributes, the following figure illustrates a simple user aggregate:

图片

Sample code for this model is shown below:

Java code:

package ;

import .*;
import lombok.*;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import .;
import .;

import .*;

/**
 * subscribers
 * <p>
 * This document was prepared by[cap4j-ddd-codegen-maven-plugin]generating
 * warnings:Do not manually modify the field statements in this document,重新generating会覆盖字段声明
 */
/* @AggregateRoot */
@Entity
@Table(name = "`user`")
@DynamicInsert
@DynamicUpdate

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class User {

    // 【Behavioral approach to start】

    public void init() {
        (()
                .user(this)
                .build(), this);
    }

    public void changeEmail(String email) {
         = email;
        (()
                .user(this)
                .build(), this);
    }

    // 【End of Behavioral Approach】


    // 【Field mapping begins】This paragraph was introduced by[cap4j-ddd-codegen-maven-plugin]safeguard,Please do not make manual changes

    @Id
    @GeneratedValue(generator = ".")
    @GenericGenerator(name = ".", strategy = ".")
    @Column(name = "`id`")
    Long id;


    /**
     * varchar(100)
     */
    @Column(name = "`name`")
    String name;

    /**
     * varchar(100)
     */
    @Column(name = "`email`")
    String email;

    // 【End of field mapping】This paragraph was introduced by[cap4j-ddd-codegen-maven-plugin]safeguard,Please do not make manual changes
}

C# code:

图片

Domain events are defined as follows:

Java code:

package ;

import ;
import ;
import ;
import ;
import .;

/**
 * User Created Events
 */
@DomainEvent
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserCreatedDomainEvent {
    User user;
}
package ;

import ;
import ;
import ;
import ;
import .;
/**
 * User mailbox change event
 */
@DomainEvent
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserEmailChangedDomainEvent {
    User user;
}

C# code:

//Defining domain events
using ;
namespace YourNamespace;

public record UserCreatedDomainEvent(User user) : IDomainEvent;

public record UserEmailChangedDomainEvent(User user) : IDomainEvent;

At this point, the code for one of our domain models is complete.

Key elements outside the domain model

Let's go back to the "model anthropomorphism" analogy and imagine how a task is accomplished in a business, the figure below illustrates a typical process:

图片

If we correspond this process to a software system, we can get the following flow:

图片

Based on the correspondence above I can know the key elements other than the domain model:

  1. Controller

  2. Command and CommandHandler

  3. DomainEventHandler

Next, we explain each of these parts

Controller

Developers who have experience in web project development are no strangers to Controller, which is the entry point for web services to interact with the front-end, where Controller's main responsibility is:

  1. Receive external input

  2. Assembles information such as request input and current user session into commands.

  3. Issue/execute commands

  4. Response Command Execution Result

Java code:

package ;

import ._share.ResponseData;
import ;
import .;
import .slf4j.Slf4j;
import ;
import ;
import ;
import ;
import ;

import ;

/**
 * user controller
 */
@Tag(name = "subscribers")
@RestController
@RequestMapping(value = "/api/user")
@Slf4j
public class UserController {

    @Autowired
     createUserCommandHandler;

    @PostMapping("/")
    public ResponseData<Long> createUserCommand(@RequestBody @Valid CreateUserCommand cmd) {
        Long result = (cmd);
        return (result);
    }
}

C# code:

[Route("api/[controller]")]
[ApiController]
public class UserController(IMediator mediator) : ControllerBase
{
    [HttpPost]
    public async Task<ResponseData<UserId>> Post([FromBody] CreateUserRequest request)
    {
        var cmd = new CreateUserCommand(, );
        var id = await (cmd);
        return ();
    }
}

===

===

Command and CommandHandler

Based on the previous correspondence, Command corresponds to Task, so we can understand it like this:

  1. Command is the information needed to perform the task

  2. CommandHandler is responsible for passing command information to the domain model

  3. CommandHandler ends up persisting the domain model

Here is a simple example:

Java code:

package ;

import ;
import ;
import ;
import ;
import ;
import ;
import .slf4j.Slf4j;
import .;
import .;
import .;
import ;


/**
 * Create User Command
 */
@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 = ()
                    .name()
                    .email()
                    .build();
            ();
            (user);
            ();
            return ();
        }
    }
}

C# code:

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(, );
        user = await (user, cancellationToken);
        return ;
    }
}

===

===

DomainEventHandler

When our command execution is complete, the domain model generates a domain event, so caring about the domain event and expecting to perform some action when the domain event occurs can be done using DomainEventHandler:

  1. DomainEventHandler generates a new command based on the event information and issues the

  2. Each DomainEventHandler does one thing, i.e., issues only one command

Java code:

package ;

import ;
import ;
import ;
import ;
import ;

/**
 * User-created domain events
 */
@Service
@RequiredArgsConstructor
public class UserCreatedDomainEventHandler {
    private final handler;

    @EventListener()
    public void handle(UserCreatedDomainEvent event) {
        (()
                .param(().getId())
                .build());
    }
}

C# code:

public class UserCreatedDomainEventHandler(IMediator mediator) 
           : IDomainEventHandler<UserCreatedDomainEvent>
{
    public Task Handle(UserCreatedDomainEvent notification, CancellationToken cancellationToken)
    {
        return (new DoSomethingCommand(), cancellationToken);
    }
}

===

===

Model persistence

In the previous article, we have been emphasizing the idea that "forget about the database when designing the model", so how do we store the model in the database after we finish designing the model? Usually we will use the storage pattern in charge of the model "access" operations, the following code illustrates the basic capabilities of a storage and the definition of the storage, slightly different is that we implement thework unit mode(UnitOfWork) to shield the database "add, delete, change, and check" semantics, we only need to "take out the model", "manipulate the model" from the warehouse, We only need to "take out the model", "manipulate the model", and "save the model" from the storage.

Java code:

package ;

import ;

/**
 * This file was generated by [cap4j-ddd-codegen-maven-plugin].
 */
public interface UserRepository extends . <User, Long> {
    // [custom code start] Code outside of this paragraph is maintained by [cap4j-ddd-codegen-maven-plugin], please do not manually change it!

    @
    public static class UserJpaRepositoryAdapter extends . <User, Long>
{
        public UserJpaRepositoryAdapter(<User> jpaSpecificationExecutor, <User, Long> jpaRepository) {
            super(jpaSpecificationExecutor, jpaRepository); }
        }
    }

    // [End of custom code] Code outside of this paragraph is maintained by [cap4j-ddd-codegen-maven-plugin], please do not change it manually!
}

C# code:

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
{
}

===

===

Processing of queries

The following shows the code for a simple query

Java code:

package ;

import ._share.;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import .slf4j.Slf4j;
import .;
import .;
import ;


/**
 * user search
 */
@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 = ((
                    root -> ().eq()
            )).orElseThrow(() -> new KnownException("non-existent"));

            return ()
                    .id(())
                    .name(())
                    .email(())
                    .build();
        }
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public static class UserQueryDto {
        private Long id;
        private String name;
        private String email;
    }
}

C# code:

public class UserQuery(ApplicationDbContext applicationDbContext)
{
    public async Task<UserDto?> QueryOrder(UserId userId, CancellationToken cancellationToken)
    {
        return await (p =>  == userId)
            .Select(p => new UserDto(, )).SingleOrDefault();
    }
}

===

===

CQRS seems to be the only positive solution

We are in the actual software system, the query is often complex scenarios, different query requirements, may break the model as a whole, it is clear that the use of the domain model itself to meet these needs is unrealistic, then the need for demand scenarios, the organization of the corresponding data structure as an output, which coincides with the "CQRS" model, or "CQRS" was proposed to solve this problem, and this pattern with the "command-event" thinking as one. This coincides with the "CQRS" pattern, or "CQRS" was proposed to solve this problem, and this pattern is integrated with the "command-event" thinking, as evidenced by the previous code example, so we believe that the practice of DDD landing, we need to use the CQRS pattern. Therefore, we believe that the practice of DDD can be realized with the help of the CQRS pattern.

图片

source code documentation

The examples in this article use cap4j (Java) and netcorepal-cloud-framework (dotnet) respectively, and you are welcome to participate in the project discussion and contribution, the project address is as follows:

/netcorepal/cap4j

/netcorepal/netcorepal-cloud-framework