Clean Architecture下拆分Use Case的最优方案探讨
基于Clean Architecture的异步操作拆分方案
针对你的场景,核心目标是将**同步核心操作(创建并保存Todo)与异步非核心操作(通知调度、短链生成)**解耦,让主流程快速返回结果,同时遵循Clean Architecture的分层原则。以下是两种落地性强且复杂度可控的方案:
1. 领域事件驱动(最贴合Clean Architecture设计)
利用领域事件将Todo创建的状态变化与后续操作解耦,符合领域建模的单一职责原则:
步骤1:Core层定义领域事件
在Project.Core中添加领域事件实体和触发逻辑,让Todo实体自身发布状态变化事件:
// 通用领域事件标记接口 public interface IDomainEvent { } // Todo创建完成的领域事件 public class TodoCreatedDomainEvent : IDomainEvent { public Guid TodoId { get; } public string Title { get; } public TodoCreatedDomainEvent(Guid todoId, string title) { TodoId = todoId; Title = title; } } // 修改Todo实体,添加事件存储逻辑 public class Todo { private readonly List<IDomainEvent> _domainEvents = new(); public Guid Id { get; private set; } public string Title { get; private set; } public string? ShortLink { get; private set; } public Todo(Guid id, string title) { Id = id; Title = title; // 创建时自动触发领域事件 _domainEvents.Add(new TodoCreatedDomainEvent(id, title)); } // 供外部调用更新短链的方法 public void UpdateShortLink(string shortLink) { ShortLink = shortLink; } // 暴露领域事件集合 public IReadOnlyCollection<IDomainEvent> GetDomainEvents() => _domainEvents.AsReadOnly(); public void ClearDomainEvents() => _domainEvents.Clear(); }
步骤2:Application层实现事件处理器
将后续操作封装为独立的事件处理器,各自处理异常、重试逻辑:
// 处理通知调度的处理器 public class TodoCreatedNotificationHandler : IDomainEventHandler<TodoCreatedDomainEvent> { private readonly INotificationScheduler _notificationScheduler; public TodoCreatedNotificationHandler(INotificationScheduler notificationScheduler) { _notificationScheduler = notificationScheduler; } public async Task Handle(TodoCreatedDomainEvent @event, CancellationToken cancellationToken) { // 独立实现异常捕获与重试 try { await _notificationScheduler.ScheduleTodoAddedNotification(@event.TodoId, cancellationToken); } catch (Exception ex) { // 记录日志或加入重试队列 // 不影响主流程 } } } // 处理短链生成的处理器 public class TodoCreatedShortLinkHandler : IDomainEventHandler<TodoCreatedDomainEvent> { private readonly IShortLinkGenerator _shortLinkGenerator; private readonly ITodoRepository _todoRepository; public TodoCreatedShortLinkHandler(IShortLinkGenerator shortLinkGenerator, ITodoRepository todoRepository) { _shortLinkGenerator = shortLinkGenerator; _todoRepository = todoRepository; } public async Task Handle(TodoCreatedDomainEvent @event, CancellationToken cancellationToken) { try { var shortLink = await _shortLinkGenerator.GenerateForTodo(@event.TodoId, cancellationToken); var todo = await _todoRepository.GetById(@event.TodoId, cancellationToken); todo.UpdateShortLink(shortLink); await _todoRepository.Update(todo, cancellationToken); } catch (Exception ex) { // 独立处理异常逻辑 } } }
步骤3:修改CreateTodo命令主流程
主命令只负责核心的Todo创建与保存,异步发布领域事件:
public class CreateTodoCommandHandler : ICommandHandler<CreateTodoCommand, Guid> { private readonly ITodoRepository _todoRepository; private readonly IDomainEventBus _domainEventBus; public CreateTodoCommandHandler(ITodoRepository todoRepository, IDomainEventBus domainEventBus) { _todoRepository = todoRepository; _domainEventBus = domainEventBus; } public async Task<Guid> Handle(CreateTodoCommand request, CancellationToken cancellationToken) { var todo = new Todo(Guid.NewGuid(), request.Title); await _todoRepository.Add(todo, cancellationToken); // 异步发布领域事件,不等待后续操作完成 foreach (var domainEvent in todo.GetDomainEvents()) { _ = _domainEventBus.PublishAsync(domainEvent, cancellationToken); } todo.ClearDomainEvents(); return todo.Id; } }
步骤4:基础设施层实现轻量事件总线
可以先基于内存实现简单版本,后续可扩展为消息队列(如RabbitMQ):
public interface IDomainEventBus { Task PublishAsync<TEvent>(TEvent domainEvent, CancellationToken cancellationToken) where TEvent : IDomainEvent; } public class InMemoryDomainEventBus : IDomainEventBus { private readonly IServiceProvider _serviceProvider; public InMemoryDomainEventBus(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public async Task PublishAsync<TEvent>(TEvent domainEvent, CancellationToken cancellationToken) where TEvent : IDomainEvent { // 异步执行处理器,不阻塞主流程 _ = Task.Run(async () => { using var scope = _serviceProvider.CreateScope(); var handlers = scope.ServiceProvider.GetServices<IDomainEventHandler<TEvent>>(); foreach (var handler in handlers) { await handler.Handle(domainEvent, cancellationToken); } }, cancellationToken); } }
2. 核心命令+后台任务队列(极简实现)
如果不想引入领域事件,可直接将后续操作拆分为独立命令,通过轻量后台队列执行:
步骤1:定义后台命令与处理器
// 通知调度命令 public record ScheduleTodoNotificationCommand(Guid TodoId); public class ScheduleTodoNotificationHandler : ICommandHandler<ScheduleTodoNotificationCommand> { private readonly INotificationScheduler _notificationScheduler; public ScheduleTodoNotificationHandler(INotificationScheduler notificationScheduler) { _notificationScheduler = notificationScheduler; } public async Task Handle(ScheduleTodoNotificationCommand request, CancellationToken cancellationToken) { // 独立处理异常与重试 await _notificationScheduler.ScheduleTodoAddedNotification(request.TodoId, cancellationToken); } } // 短链生成命令类似,此处省略
步骤2:实现后台命令队列与处理器
用IHostedService实现轻量后台消费服务,替代复杂的Worker Service:
public interface IBackgroundCommandQueue { Task EnqueueAsync<TCommand>(TCommand command, CancellationToken cancellationToken) where TCommand : ICommand; Task<TCommand> DequeueAsync<TCommand>(CancellationToken cancellationToken) where TCommand : ICommand; } public class BackgroundCommandProcessor : BackgroundService { private readonly IBackgroundCommandQueue _queue; private readonly IServiceProvider _serviceProvider; public BackgroundCommandProcessor(IBackgroundCommandQueue queue, IServiceProvider serviceProvider) { _queue = queue; _serviceProvider = serviceProvider; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { // 消费队列中的命令 var notificationCmd = await _queue.DequeueAsync<ScheduleTodoNotificationCommand>(stoppingToken); using var scope = _serviceProvider.CreateScope(); var handler = scope.ServiceProvider.GetRequiredService<ICommandHandler<ScheduleTodoNotificationCommand>>(); await handler.Handle(notificationCmd, stoppingToken); // 同理处理短链生成命令 var shortLinkCmd = await _queue.DequeueAsync<GenerateTodoShortLinkCommand>(stoppingToken); var shortLinkHandler = scope.ServiceProvider.GetRequiredService<ICommandHandler<GenerateTodoShortLinkCommand>>(); await shortLinkHandler.Handle(shortLinkCmd, stoppingToken); } } }
步骤3:修改CreateTodo命令
public class CreateTodoCommandHandler : ICommandHandler<CreateTodoCommand, Guid> { private readonly ITodoRepository _todoRepository; private readonly IBackgroundCommandQueue _backgroundQueue; public CreateTodoCommandHandler(ITodoRepository todoRepository, IBackgroundCommandQueue backgroundQueue) { _todoRepository = todoRepository; _backgroundQueue = backgroundQueue; } public async Task<Guid> Handle(CreateTodoCommand request, CancellationToken cancellationToken) { var todo = new Todo(Guid.NewGuid(), request.Title); await _todoRepository.Add(todo, cancellationToken); // 将后台命令加入队列,异步执行 await _backgroundQueue.EnqueueAsync(new ScheduleTodoNotificationCommand(todo.Id), cancellationToken); await _backgroundQueue.EnqueueAsync(new GenerateTodoShortLinkCommand(todo.Id), cancellationToken); return todo.Id; } }
方案选择建议
| 方案 | 优势 | 适用场景 |
|---|---|---|
| 领域事件驱动 | 完全符合Clean Architecture领域建模,解耦性极强,便于后续扩展业务逻辑 | 后续操作与领域状态强关联的场景 |
| 后台任务队列 | 实现极简,逻辑直观 | 后续操作无强领域关联性的场景 |
两种方案都能实现主流程快速返回,异步操作独立处理异常、重试等逻辑,避免主命令逻辑臃肿。
内容的提问来源于stack exchange,提问作者RussianPupok Pupok
相关产品推荐
相关产品推荐

