怎么写出简洁的CQRS代码

发布时间:2021-11-15 16:13:53 作者:iii
来源:亿速云 阅读:174
# 怎么写出简洁的CQRS代码

## 前言

CQRS(Command Query Responsibility Segregation)是一种将读写操作分离的架构模式。通过将命令(写操作)和查询(读操作)分离到不同的模型中,可以提高系统的可扩展性、性能和安全性。然而,实现CQRS时容易陷入过度设计的陷阱,导致代码复杂度陡增。本文将探讨如何用简洁的方式实现CQRS模式。

## 一、理解CQRS的核心思想

### 1.1 CQRS的基本概念

CQRS的核心是将系统分为两个部分:
- **命令侧(Command)**:处理创建、更新和删除操作(CUD)
- **查询侧(Query)**:处理数据读取操作(R)

### 1.2 何时使用CQRS

适合场景:
- 读写负载差异大的系统
- 需要不同数据模型的场景
- 需要优化查询性能的场景

不适合场景:
- 简单CRUD应用
- 读写模型基本一致的系统

## 二、简洁实现CQRS的关键原则

### 2.1 保持简单

```csharp
// 不好的例子:过度抽象的命令处理器
public interface ICommandHandler<TCommand> where TCommand : ICommand
{
    Task HandleAsync(TCommand command);
}

// 好的例子:简单的命令处理
public class CreateOrderCommand
{
    public string ProductId { get; set; }
    public int Quantity { get; set; }
}

public class OrderService
{
    public async Task Handle(CreateOrderCommand command)
    {
        // 直接的处理逻辑
    }
}

2.2 避免过早优化

不要一开始就考虑事件溯源、复杂消息总线等高级概念。从简单的分离开始:

传统CRUD:
Controller -> Service -> Repository

简单CQRS:
CommandController -> CommandService -> CommandRepository
QueryController -> QueryService -> QueryRepository

2.3 渐进式演进

从简单分离开始,根据需要逐步引入: 1. 先分离读写模型 2. 再考虑不同存储 3. 最后引入事件溯源等高级特性

三、命令侧实现技巧

3.1 命令设计

// 好的命令设计示例
public class UpdateUserEmailCommand
{
    public Guid UserId { get; }
    public string NewEmail { get; }
    
    public UpdateUserEmailCommand(Guid userId, string newEmail)
    {
        UserId = userId;
        NewEmail = newEmail;
    }
}

3.2 命令验证

public class UpdateUserEmailCommandValidator : AbstractValidator<UpdateUserEmailCommand>
{
    public UpdateUserEmailCommandValidator()
    {
        RuleFor(cmd => cmd.UserId).NotEmpty();
        RuleFor(cmd => cmd.NewEmail).NotEmpty().EmailAddress();
    }
}

3.3 命令处理

public class UpdateUserEmailHandler
{
    private readonly IUserRepository _repository;
    
    public UpdateUserEmailHandler(IUserRepository repository)
    {
        _repository = repository;
    }
    
    public async Task Handle(UpdateUserEmailCommand command)
    {
        var user = await _repository.GetByIdAsync(command.UserId);
        user.UpdateEmail(command.NewEmail);
        await _repository.SaveAsync(user);
    }
}

四、查询侧实现技巧

4.1 查询设计

public class GetUserDetailsQuery
{
    public Guid UserId { get; }
    
    public GetUserDetailsQuery(Guid userId)
    {
        UserId = userId;
    }
}

4.2 查询优化

public class UserDetailsDto
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    // 只包含视图需要的字段
}

public class GetUserDetailsHandler
{
    private readonly IUserQueryRepository _queryRepository;
    
    public async Task<UserDetailsDto> Handle(GetUserDetailsQuery query)
    {
        return await _queryRepository.GetUserDetailsAsync(query.UserId);
    }
}

4.3 使用DTO投影

// 在查询存储库中直接返回DTO
public interface IUserQueryRepository
{
    Task<UserDetailsDto> GetUserDetailsAsync(Guid userId);
}

// EF Core实现示例
public async Task<UserDetailsDto> GetUserDetailsAsync(Guid userId)
{
    return await _context.Users
        .Where(u => u.Id == userId)
        .Select(u => new UserDetailsDto
        {
            Id = u.Id,
            Name = u.Name,
            Email = u.Email
        })
        .FirstOrDefaultAsync();
}

五、同步读写模型的策略

5.1 简单同步方式

// 命令处理完成后同步更新读模型
public class UpdateUserEmailHandler
{
    // ... 其他代码
    
    public async Task Handle(UpdateUserEmailCommand command)
    {
        // 更新写模型
        var user = await _writeRepository.GetByIdAsync(command.UserId);
        user.UpdateEmail(command.NewEmail);
        await _writeRepository.SaveAsync(user);
        
        // 同步更新读模型
        await _readRepository.UpdateEmailAsync(command.UserId, command.NewEmail);
    }
}

5.2 使用领域事件

// 定义领域事件
public class UserEmailUpdatedEvent
{
    public Guid UserId { get; }
    public string NewEmail { get; }
    
    public UserEmailUpdatedEvent(Guid userId, string newEmail)
    {
        UserId = userId;
        NewEmail = newEmail;
    }
}

// 命令处理中发布事件
public async Task Handle(UpdateUserEmailCommand command)
{
    // ... 更新逻辑
    
    // 发布事件
    await _eventPublisher.PublishAsync(
        new UserEmailUpdatedEvent(command.UserId, command.NewEmail));
}

// 事件处理器更新读模型
public class UserEmailUpdatedEventHandler
{
    private readonly IUserQueryRepository _readRepository;
    
    public async Task Handle(UserEmailUpdatedEvent @event)
    {
        await _readRepository.UpdateEmailAsync(@event.UserId, @event.NewEmail);
    }
}

六、测试策略

6.1 命令测试

[Test]
public async Task UpdateUserEmail_Should_UpdateEmailInWriteModel()
{
    // 准备
    var userId = Guid.NewGuid();
    var originalEmail = "old@example.com";
    var newEmail = "new@example.com";
    
    var user = new User(userId, "test", originalEmail);
    var mockRepo = new Mock<IUserRepository>();
    mockRepo.Setup(r => r.GetByIdAsync(userId)).ReturnsAsync(user);
    
    var handler = new UpdateUserEmailHandler(mockRepo.Object);
    
    // 执行
    await handler.Handle(new UpdateUserEmailCommand(userId, newEmail));
    
    // 验证
    user.Email.Should().Be(newEmail);
    mockRepo.Verify(r => r.SaveAsync(user), Times.Once);
}

6.2 查询测试

[Test]
public async Task GetUserDetails_Should_ReturnCorrectDto()
{
    // 准备
    var userId = Guid.NewGuid();
    var expectedDto = new UserDetailsDto 
    { 
        Id = userId,
        Name = "Test",
        Email = "test@example.com"
    };
    
    var mockRepo = new Mock<IUserQueryRepository>();
    mockRepo.Setup(r => r.GetUserDetailsAsync(userId))
        .ReturnsAsync(expectedDto);
    
    var handler = new GetUserDetailsHandler(mockRepo.Object);
    
    // 执行
    var result = await handler.Handle(new GetUserDetailsQuery(userId));
    
    // 验证
    result.Should().BeEquivalentTo(expectedDto);
}

七、常见陷阱与解决方案

7.1 过度设计问题

问题表现: - 过早引入复杂事件总线 - 为每个命令/查询创建过多抽象层 - 在不必要的情况下使用事件溯源

解决方案: 从简单实现开始,随着需求演进逐步增加复杂性。

7.2 数据一致性问题

问题表现: - 读写模型不一致 - 同步延迟导致用户体验问题

解决方案: 1. 对于关键数据,采用同步更新 2. 对于非关键数据,接受最终一致性 3. 提供用户界面反馈机制

7.3 代码重复问题

问题表现: - 读写模型中有重复的验证逻辑 - 相似的DTO定义

解决方案: 1. 共享验证逻辑(如FluentValidation规则) 2. 使用代码生成工具减少重复 3. 在适当层级共享简单DTO

八、演进到高级CQRS

当简单CQRS不能满足需求时,可以考虑:

8.1 引入事件溯源

// 事件溯源示例
public class User : EventSourcedAggregate
{
    public string Name { get; private set; }
    public string Email { get; private set; }
    
    public void UpdateEmail(string newEmail)
    {
        Apply(new UserEmailUpdatedEvent(Id, newEmail));
    }
    
    protected override void When(object @event)
    {
        switch (@event)
        {
            case UserEmailUpdatedEvent e:
                Email = e.NewEmail;
                break;
        }
    }
}

8.2 引入消息总线

// 使用MediatR实现进程内消息总线
public class UpdateUserEmailCommand : IRequest<Unit> { ... }

public class UpdateUserEmailHandler : IRequestHandler<UpdateUserEmailCommand, Unit>
{
    public async Task<Unit> Handle(UpdateUserEmailCommand request, CancellationToken ct)
    {
        // 处理逻辑
        return Unit.Value;
    }
}

// 控制器调用
[HttpPut("email")]
public async Task<IActionResult> UpdateEmail([FromBody] UpdateUserEmailCommand command)
{
    await _mediator.Send(command);
    return Ok();
}

结语

简洁的CQRS实现关键在于: 1. 从简单分离开始 2. 避免过早优化 3. 渐进式演进 4. 根据实际需求调整复杂度

记住,CQRS是一种架构模式,而不是目标本身。最适合你项目的实现,才是最好的实现。


本文共计约3700字,涵盖了从CQRS基础概念到简洁实现的全过程,并提供了代码示例和实用建议。希望对你实现简洁有效的CQRS架构有所帮助。 “`

推荐阅读:
  1. 几点建议帮你写出简洁的JS代码
  2. 怎样用PHP编写出简洁的代码

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

cqrs

上一篇:Unity+shader如何绘制halftone动画实现星之卡比新星同盟切屏效果

下一篇:JavaScript中怎么实现返回提示信息

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》