整洁代码的幻象:我们为何最终放弃了 MediatR

作者:微信公众号:【架构师老卢】
8-26 18:52
9

起初,MediatR 如同魔法一般。 它解耦了一切。不再有服务之间的面条式耦合。只有整洁的 IMediator.Send() 调用,像行为良好的无人机一样穿梭于我们的处理程序之间。 有一段时间,它工作得很好——尤其是在我们早期的清洁架构(Clean Architecture)实验中。

但在我们开发第三个微服务和经历第十次生产事故之间的某个时候,那些整洁的代码变成了一场调试噩梦。

当抽象发生泄漏,日志随之消亡 事情是这样的。

我们正在扩展一个拥有数十个松散耦合微服务的系统。每个服务内部使用 MediatR 来处理命令(Command)和查询(Query)。初衷是高尚的:关注点分离、可测试性,以及无处不在的单一职责原则。

但抽象变成了一种蒙蔽。 当用户在前端点击“提交”而请求在半路失败时,我们完全不知道原因。

调用栈?被混淆了。 日志?分散各处。 错误?被埋藏在三层处理程序之深。

我们的一位开发人员——暂且叫他 Raj——花了六个小时追踪一个空引用(null ref)bug,这个 bug 被一个预处理程序(pre-handler)默默地吞掉了。结果发现管道注入的作用域服务与预期不同。没有异常。没有提示。只有一片寂静。

那时我们明白了:出问题了。

这不仅仅是调试的痛苦。以下是 MediatR 对我们的项目造成的真实影响:

  1. 反射和管道开销带来的性能损耗 每次 Send() 都会触发一连串的行为(Behavior)——日志记录、验证、授权——即使对于简单的查询也是如此。在热点路径上(如获取用户仪表板的公共 API),这会快速累积。

对一个端点进行重构前后的基准测试:

// 使用 MediatR (管道行为 + 处理程序)
var result = await _mediator.Send(new GetUserDashboardQuery(userId));

// 重构后 (直接服务调用)
var result = await _dashboardService.GetUserDashboardAsync(userId);

| 方法 | 平均响应时间 | | :------------- | :----------- | | MediatR | 118ms | | 直接服务调用 | 52ms |

仅仅是抽象开销,就慢了超过 2 倍。

  1. 测试变得脆弱和冗长 因为 MediatR 将逻辑拆分到处理程序和行为中,单元测试的设置复杂性急剧增加。模拟(Mock)每个注入的依赖——包括管道行为——变成了一件苦差事。

我们曾经有这样的测试:

var mediator = new Mock<IMediator>();
mediator.Setup(m => m.Send(It.IsAny<MyCommand>(), default))
        .ReturnsAsync(new MyResult());

var sut = new MyController(mediator.Object);
// 执行操作 (Act) + 断言 (Assert)

弃用 MediatR 后:

var dashboardService = new Mock<IDashboardService>();
dashboardService.Setup(s => s.GetUserDashboardAsync(userId))
                .ReturnsAsync(new DashboardData());

var sut = new DashboardController(dashboardService.Object);
// 执行操作 (Act) + 断言 (Assert)

更简单。更可读。更少的模拟。我们终于是在测试行为,而不是管道代码。

  1. 开发人员上手速度变慢 新员工觉得 MediatR 令人困惑。他们会问:“这个命令在哪里处理的?” 我们需要打开四个文件——控制器(Controller)、命令(Command)、处理程序(Handler)和验证器(Validator)——仅仅是为了解释一个简单的流程。理论上很整洁。实践中令人迷失方向。

一位初级开发人员说得很好:

“感觉就像我在玩‘你丢我捡’(fetch)游戏来进行调试。”

我们转向了:更简单的事件驱动模式 我们不再默认在所有地方使用 MediatR,而是采用了一种混合的事件驱动模式,它结合了服务层调用和领域事件(Domain Event)。以下是转变:

  • 查询(Query) 现在直接调用服务。没有处理程序的间接性。
  • 命令(Command) 保持简单,调用应用服务(Application Service)中的用例方法。
  • 副作用 通过领域事件触发,这些事件在聚合根(Aggregate)内部发布。

我们实现了一个轻量级的领域事件分发器,如下所示:

public interface IDomainEvent { }
public interface IEventHandler<T> where T : IDomainEvent
{
    Task HandleAsync(T @event);
}

事件在工作单元(Unit of Work)提交后发布:

public class User
{
    public void Activate()
    {
        IsActive = true;
        AddDomainEvent(new UserActivatedEvent(Id));
    }
}

处理程序保持独立,即发即忘(fire-and-forget):

public class SendWelcomeEmailHandler : IEventHandler<UserActivatedEvent>
{
    public Task HandleAsync(UserActivatedEvent @event)
    {
        return _emailService.SendWelcomeEmailAsync(@event.UserId);
    }
}

这是显式的。可追踪的。代码读起来像一个故事——而不是一本悬疑小说。

MediatR 在哪些场景下仍然适用 让我澄清:MediatR 并不坏。 它只是不再是我们默认的选择。

我们仍然在以下场景中使用它:

  • 内部工具中,其中 CQRS 显得过于繁重
  • 在单体应用中协调横切关注点(Cross-Cutting Concerns)
  • 原型应用开发,团队规模小且速度至关重要

但是对于大型的、生产级别的系统? 我们更倾向于显性化而非纯粹性。

最后的思考:过度抽象的代价 清洁架构的拥趸们喜欢 MediatR,因为它满足了所有条件:SOLID、DDD、CQRS。但如果你不小心,你会构建一个完全解耦的系统——从现实中解耦。

如果你的目标是可维护、可观测和高性能的软件,问问自己:

我这里真的需要一个中介者(Mediator)吗? 或者我是在解决一个尚未存在的问题?

在我们的案例中,移除 MediatR 不仅仅是清理了架构——它还明确了所有权、降低了延迟,并让开发人员更开心。

这比教科书上的图表更有价值。

欢迎鼓掌(Claps)。特别是如果你曾经愤怒地调试过管道行为(pipeline behaviour)。 我们来聊聊:你还在使用 MediatR 吗?你会停止使用吗?

完整代码示例 下面的示例演示了本文中关于放弃 MediatR 的关键点。这将通过实际示例展示“之前”和“之后”的方法。

// =============================================================================
// 之前: 使用 MediatR 模式
// =============================================================================

// 命令/查询定义
public class GetUserDashboardQuery : IRequest<DashboardData>
{
    public int UserId { get; set; }
}

public class ActivateUserCommand : IRequest<bool>
{
    public int UserId { get; set; }
}

// MediatR 处理程序
public class GetUserDashboardHandler : IRequestHandler<GetUserDashboardQuery, DashboardData>
{
    private readonly IUserRepository _userRepository;
    private readonly IDashboardService _dashboardService;

    public GetUserDashboardHandler(IUserRepository userRepository, IDashboardService dashboardService)
    {
        _userRepository = userRepository;
        _dashboardService = dashboardService;
    }

    public async Task<DashboardData> Handle(GetUserDashboardQuery request, CancellationToken cancellationToken)
    {
        var user = await _userRepository.GetByIdAsync(request.UserId);
        if (user == null) throw new UserNotFoundException();
        
        return await _dashboardService.BuildDashboardAsync(user);
    }
}

public class ActivateUserHandler : IRequestHandler<ActivateUserCommand, bool>
{
    private readonly IUserRepository _userRepository;
    private readonly IEmailService _emailService;

    public ActivateUserHandler(IUserRepository userRepository, IEmailService emailService)
    {
        _userRepository = userRepository;
        _emailService = emailService;
    }

    public async Task<bool> Handle(ActivateUserCommand request, CancellationToken cancellationToken)
    {
        var user = await _userRepository.GetByIdAsync(request.UserId);
        if (user == null) return false;

        user.Activate();
        await _userRepository.UpdateAsync(user);
        
        // 副作用处理内联
        await _emailService.SendWelcomeEmailAsync(user.Id);
        
        return true;
    }
}

// 管道行为 (增加了复杂性)
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        _logger.LogInformation($"Handling {typeof(TRequest).Name}");
        var response = await next();
        _logger.LogInformation($"Handled {typeof(TRequest).Name}");
        return response;
    }
}

// 使用 MediatR 的控制器
[ApiController]
[Route("[controller]")]
public class DashboardController : ControllerBase
{
    private readonly IMediator _mediator;

    public DashboardController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpGet("{userId}")]
    public async Task<DashboardData> GetUserDashboard(int userId)
    {
        return await _mediator.Send(new GetUserDashboardQuery { UserId = userId });
    }

    [HttpPost("{userId}/activate")]
    public async Task<bool> ActivateUser(int userId)
    {
        return await _mediator.Send(new ActivateUserCommand { UserId = userId });
    }
}

// 使用 MediatR 的复杂测试设置
[Test]
public async Task GetUserDashboard_ShouldReturnDashboard_WhenUserExists()
{
    // 安排 (Arrange)
    var mediator = new Mock<IMediator>();
    var expectedDashboard = new DashboardData { UserId = 1, Name = "Test User" };
    
    mediator.Setup(m => m.Send(It.IsAny<GetUserDashboardQuery>(), default))
            .ReturnsAsync(expectedDashboard);

    var controller = new DashboardController(mediator.Object);

    // 执行 (Act)
    var result = await controller.GetUserDashboard(1);

    // 断言 (Assert)
    Assert.AreEqual(expectedDashboard, result);
    mediator.Verify(m => m.Send(It.IsAny<GetUserDashboardQuery>(), default), Times.Once);
}

// =============================================================================
// 之后: 直接服务调用与领域事件方法
// =============================================================================

// 领域事件
public interface IDomainEvent { }

public class UserActivatedEvent : IDomainEvent
{
    public int UserId { get; }
    public DateTime OccurredAt { get; }

    public UserActivatedEvent(int userId)
    {
        UserId = userId;
        OccurredAt = DateTime.UtcNow;
    }
}

// 事件处理程序接口
public interface IEventHandler<T> where T : IDomainEvent
{
    Task HandleAsync(T @event);
}

// 领域事件分发器
public interface IDomainEventDispatcher
{
    Task DispatchAsync<T>(T @event) where T : IDomainEvent;
}

public class DomainEventDispatcher : IDomainEventDispatcher
{
    private readonly IServiceProvider _serviceProvider;

    public DomainEventDispatcher(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public async Task DispatchAsync<T>(T @event) where T : IDomainEvent
    {
        var handlers = _serviceProvider.GetServices<IEventHandler<T>>();
        var tasks = handlers.Select(handler => handler.HandleAsync(@event));
        await Task.WhenAll(tasks);
    }
}

// 增强的领域实体 (包含事件)
public class User
{
    private readonly List<IDomainEvent> _domainEvents = new();

    public int Id { get; private set; }
    public string Name { get; private set; }
    public bool IsActive { get; private set; }

    public void Activate()
    {
        if (!IsActive)
        {
            IsActive = true;
            AddDomainEvent(new UserActivatedEvent(Id));
        }
    }

    public void AddDomainEvent(IDomainEvent @event)
    {
        _domainEvents.Add(@event);
    }

    public IReadOnlyList<IDomainEvent> GetDomainEvents() => _domainEvents.AsReadOnly();
    public void ClearDomainEvents() => _domainEvents.Clear();
}

// 应用服务 (直接调用,无 MediatR)
public interface IUserService
{
    Task<DashboardData> GetUserDashboardAsync(int userId);
    Task<bool> ActivateUserAsync(int userId);
}

public class UserService : IUserService
{
    private readonly IUserRepository _userRepository;
    private readonly IDashboardService _dashboardService;
    private readonly IDomainEventDispatcher _eventDispatcher;

    public UserService(
        IUserRepository userRepository, 
        IDashboardService dashboardService,
        IDomainEventDispatcher eventDispatcher)
    {
        _userRepository = userRepository;
        _dashboardService = dashboardService;
        _eventDispatcher = eventDispatcher;
    }

    public async Task<DashboardData> GetUserDashboardAsync(int userId)
    {
        var user = await _userRepository.GetByIdAsync(userId);
        if (user == null) throw new UserNotFoundException();
        
        return await _dashboardService.BuildDashboardAsync(user);
    }

    public async Task<bool> ActivateUserAsync(int userId)
    {
        var user = await _userRepository.GetByIdAsync(userId);
        if (user == null) return false;

        user.Activate();
        await _userRepository.UpdateAsync(user);
        
        // 在成功持久化后分发领域事件
        var events = user.GetDomainEvents();
        foreach (var @event in events)
        {
            await _eventDispatcher.DispatchAsync(@event);
        }
        user.ClearDomainEvents();
        
        return true;
    }
}

// 事件处理程序 (分离的副作用)
public class SendWelcomeEmailHandler : IEventHandler<UserActivatedEvent>
{
    private readonly IEmailService _emailService;
    private readonly ILogger<SendWelcomeEmailHandler> _logger;

    public SendWelcomeEmailHandler(IEmailService emailService, ILogger<SendWelcomeEmailHandler> logger)
    {
        _emailService = emailService;
        _logger = logger;
    }

    public async Task HandleAsync(UserActivatedEvent @event)
    {
        _logger.LogInformation($"Sending welcome email to user {@event.UserId}");
        await _emailService.SendWelcomeEmailAsync(@event.UserId);
    }
}

public class UpdateUserStatsHandler : IEventHandler<UserActivatedEvent>
{
    private readonly IUserStatsService _userStatsService;

    public UpdateUserStatsHandler(IUserStatsService userStatsService)
    {
        _userStatsService = userStatsService;
    }

    public async Task HandleAsync(UserActivatedEvent @event)
    {
        await _userStatsService.IncrementActiveUsersAsync();
    }
}

// 简化后的控制器 (直接服务调用)
[ApiController]
[Route("[controller]")]
public class DashboardController : ControllerBase
{
    private readonly IUserService _userService;

    public DashboardController(IUserService userService)
    {
        _userService = userService;
    }

    [HttpGet("{userId}")]
    public async Task<DashboardData> GetUserDashboard(int userId)
    {
        return await _userService.GetUserDashboardAsync(userId);
    }

    [HttpPost("{userId}/activate")]
    public async Task<bool> ActivateUser(int userId)
    {
        return await _userService.ActivateUserAsync(userId);
    }
}

// 简化后的测试设置
[Test]
public async Task GetUserDashboard_ShouldReturnDashboard_WhenUserExists()
{
    // 安排 (Arrange)
    var userService = new Mock<IUserService>();
    var expectedDashboard = new DashboardData { UserId = 1, Name = "Test User" };
    
    userService.Setup(s => s.GetUserDashboardAsync(1))
               .ReturnsAsync(expectedDashboard);

    var controller = new DashboardController(userService.Object);

    // 执行 (Act)
    var result = await controller.GetUserDashboard(1);

    // 断言 (Assert)
    Assert.AreEqual(expectedDashboard, result);
    userService.Verify(s => s.GetUserDashboardAsync(1), Times.Once);
}

// =============================================================================
// 支撑类和接口
// =============================================================================

public class DashboardData
{
    public int UserId { get; set; }
    public string Name { get; set; }
    public List<string> RecentActivities { get; set; } = new();
}

public interface IUserRepository
{
    Task<User> GetByIdAsync(int id);
    Task UpdateAsync(User user);
}

public interface IDashboardService
{
    Task<DashboardData> BuildDashboardAsync(User user);
}

public interface IEmailService
{
    Task SendWelcomeEmailAsync(int userId);
}

public interface IUserStatsService
{
    Task IncrementActiveUsersAsync();
}

public class UserNotFoundException : Exception
{
    public UserNotFoundException() : base("User not found") { }
}

// =============================================================================
// 依赖注入容器设置对比
// =============================================================================

// MediatR 设置 (Startup.cs)
public void ConfigureServicesMediatR(IServiceCollection services)
{
    services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
    services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
    // ... 其他管道行为
}

// 直接服务设置 (Startup.cs)
public void ConfigureServicesDirect(IServiceCollection services)
{
    services.AddScoped<IUserService, UserService>();
    services.AddScoped<IDomainEventDispatcher, DomainEventDispatcher>();
    services.AddScoped<IEventHandler<UserActivatedEvent>, SendWelcomeEmailHandler>();
    services.AddScoped<IEventHandler<UserActivatedEvent>, UpdateUserStatsHandler>();
    // ... 其他服务
}
相关留言评论
昵称:
邮箱:
阅读排行