# 怎么寫出簡潔的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)
{
// 直接的處理邏輯
}
}
不要一開始就考慮事件溯源、復雜消息總線等高級概念。從簡單的分離開始:
傳統CRUD:
Controller -> Service -> Repository
簡單CQRS:
CommandController -> CommandService -> CommandRepository
QueryController -> QueryService -> QueryRepository
從簡單分離開始,根據需要逐步引入: 1. 先分離讀寫模型 2. 再考慮不同存儲 3. 最后引入事件溯源等高級特性
// 好的命令設計示例
public class UpdateUserEmailCommand
{
public Guid UserId { get; }
public string NewEmail { get; }
public UpdateUserEmailCommand(Guid userId, string newEmail)
{
UserId = userId;
NewEmail = newEmail;
}
}
public class UpdateUserEmailCommandValidator : AbstractValidator<UpdateUserEmailCommand>
{
public UpdateUserEmailCommandValidator()
{
RuleFor(cmd => cmd.UserId).NotEmpty();
RuleFor(cmd => cmd.NewEmail).NotEmpty().EmailAddress();
}
}
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);
}
}
public class GetUserDetailsQuery
{
public Guid UserId { get; }
public GetUserDetailsQuery(Guid userId)
{
UserId = userId;
}
}
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);
}
}
// 在查詢存儲庫中直接返回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();
}
// 命令處理完成后同步更新讀模型
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);
}
}
// 定義領域事件
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);
}
}
[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);
}
[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);
}
問題表現: - 過早引入復雜事件總線 - 為每個命令/查詢創建過多抽象層 - 在不必要的情況下使用事件溯源
解決方案: 從簡單實現開始,隨著需求演進逐步增加復雜性。
問題表現: - 讀寫模型不一致 - 同步延遲導致用戶體驗問題
解決方案: 1. 對于關鍵數據,采用同步更新 2. 對于非關鍵數據,接受最終一致性 3. 提供用戶界面反饋機制
問題表現: - 讀寫模型中有重復的驗證邏輯 - 相似的DTO定義
解決方案: 1. 共享驗證邏輯(如FluentValidation規則) 2. 使用代碼生成工具減少重復 3. 在適當層級共享簡單DTO
當簡單CQRS不能滿足需求時,可以考慮:
// 事件溯源示例
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;
}
}
}
// 使用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架構有所幫助。 “`
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。