用 MediatR 替换服务层 - 值得这样做吗?

Kon*_*rad 7 c# soa domain-driven-design mediatr asp.net-core

您认为用 MediatR 替换我的服务层或服务类是否合理?例如,我的服务类如下所示:

public interface IEntityService<TEntityDto> where TEntityDto : class, IDto
{
    Task<TEntityDto> CreateAsync(TEntityDto entityDto);
    Task<bool> DeleteAsync(int id);
    Task<IEnumerable<TEntityDto>> GetAllAsync(SieveModel sieveModel);
    Task<TEntityDto> GetByIdAsync(int id);
    Task<TEntityDto> UpdateAsync(int id, TEntityDto entityDto);
}
Run Code Online (Sandbox Code Playgroud)

我想实现某种模块化设计,以便其他动态加载的模块或插件可以为我的主要核心应用程序编写自己的通知或命令处理程序。

目前,我的应用程序根本不是事件驱动的,并且我的动态加载的插件没有简单的方法来进行通信。

我可以将 MediatR 合并到我的控制器中,完全删除服务层,或者将它与我的服务层一起使用,只是发布通知,以便我的插件可以处理它们。

目前,我的逻辑主要是 CRUD,但在创建、更新、删除之前有很多自定义逻辑正在进行。

我的服务的可能替换看起来像:

public class CommandHandler : IRequestHandler<CreateCommand, Response>, IRequestHandler<UpdateCommand, Response>, IRequestHandler<DeleteCommand, bool>
{
    private readonly DbContext _dbContext;

    public CommandHandler(DbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public Task<Response> Handle(CreateCommand request, CancellationToken cancellationToken)
    {
        //...
    }

    public Task<Response> Handle(UpdateCommand request, CancellationToken cancellationToken)
    {
        //...
    }

    public Task<bool> Handle(DeleteCommand request, CancellationToken cancellationToken)
    {
        ///...
    }
}
Run Code Online (Sandbox Code Playgroud)

这样做会不会有什么问题?

基本上,我正在努力为我的逻辑流程选择什么:

  • 控制器 -> 服务 -> MediatR -> 通知处理程序 -> 存储库
  • 控制器 -> MediatR -> 命令处理程序 -> 存储库

似乎使用 MediatR 我不能有一个单一的模型来创建、更新和删除,所以一种重用它的方法我需要派生如下请求:

public CreateRequest : MyDto, IRequest<MyDto> {}        
public UpdateRequest : MyDto, IRequest<MyDto> {} 
Run Code Online (Sandbox Code Playgroud)

或将其嵌入我的命令中,例如:

public CreateRequest : IRequest<MyDto>
{
    MyDto MyDto { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

MediatR 的一个优点是能够轻松插入和拔出逻辑,这似乎非常适合模块化架构,但我仍然有点困惑如何用它来塑造我的架构。

Sco*_*nen 25

如果你有一个类,让我们说一个 API 控制器,它取决于

IRequestHandler<CreateCommand, Response>

更改您的课程以使其取决于IMediator,有什么好处,

而不是打电话

return requestHandler.HandleRequest(request);
Run Code Online (Sandbox Code Playgroud)

它叫

return mediator.Send(request);
Run Code Online (Sandbox Code Playgroud)

结果是,我们没有注入我们需要的依赖,而是注入了一个服务定位器,它反过来解析了我们需要的依赖。

引用 Mark Seeman 的文章,

简而言之,Service Locator 的问题在于它隐藏了类的依赖关系,导致运行时错误而不是编译时错误,以及使代码更难以维护,因为不清楚何时引入中断改变。

它不完全相同

var commandHandler = serviceLocator.Resolve<IRequestHandler<CreateCommand, Response>>();
return commandHandler.Handle(request);
Run Code Online (Sandbox Code Playgroud)

因为中介器仅限于解析命令和查询处理程序,但它很接近。它仍然是一个提供对许多其他接口的访问的单一接口。

它使代码更难导航

介绍完之后IMediator,我们的类还是间接依赖的IRequestHandler<CreateCommand, Response>。不同的是,现在我们无法通过观察来判断。我们无法从界面导航到其实现。如果我们知道要查找什么——也就是说,如果我们知道命令处理程序接口名称的约定,我们可能会认为我们仍然可以遵循依赖关系。但这远不如实际声明它所依赖的类那么有用。

当然,我们可以在不编写代码的情况下将接口连接到具体的实现,但是节省的时间微不足道,而且我们可能会失去我们节省的任何时间,因为导航代码的难度增加(如果是次要的)。并且有些库无论如何都会为我们注册这些依赖项,同时仍然允许我们注入我们实际依赖的抽象。

这是一种奇怪的、扭曲的依赖抽象的方式

有人建议使用中介器有助于实现装饰器模式。但同样,我们已经通过依赖抽象获得了这种能力。我们可以使用接口的一种实现或另一种添加装饰器的实现。依赖抽象的意义在于我们可以在不改变抽象的情况下改变这样的实现细节

详细说明:依赖的重点ISomethingSpecific是我们可以在不修改依赖它的类的情况下更改或替换实现。但是如果我们说,“我想改变ISomethingSpecific(通过添加装饰器)的实现,所以为了实现这一点,我将改变依赖于 的类,这些类ISomethingSpecific工作得很好,并使它们依赖于一些泛型,通用接口”,然后出现问题。还有许多其他方法可以添加装饰器,而无需修改不需要更改的代码部分。

是的,使用会IMediator促进松散耦合。但是我们已经通过使用定义良好的抽象实现了这一点。添加一层又一层的间接性不会使这种好处成倍增加。如果您有足够的抽象,可以轻松编写单元测试,那么您就已经足够了。

模糊的依赖关系更容易违反单一职责原则

假设您有一个用于下订单的类,它取决于ICommandHandler<PlaceOrderCommand>. 如果有人试图偷偷进入不属于那里的东西,比如更新用户数据的命令,会发生什么?他们必须添加一个新的依赖项,ICommandHandler<ChangeUserAddressCommand>. 如果他们想继续在该类中堆积更多的东西,违反 SRP,会发生什么?他们将不得不继续添加更多的依赖项。这并不能阻止他们这样做,但至少它揭示了正在发生的事情。

另一方面,如果您可以在不添加更多依赖项的情况下将各种随机内容添加到类中会怎样?类依赖于可以做任何事情的抽象。它可以下订单、更新地址、请求销售历史等等,而无需添加任何新的依赖项。如果将 IoC 容器注入到不属于它的类中,则会遇到同样的问题。它是单个类或接口,可用于请求各种依赖项。这是一个服务定位器。

IMediator不会导致违反 SRP,它的缺失也不会阻止它们。但是明确的、特定的依赖引导我们远离这种违规行为。

中介者模式

奇怪的是,使用 MediatR 通常与中介模式没有任何关系。中介者模式通过让对象与中介者交互而不是直接相互交互来促进松散耦合。如果我们已经依赖于像 anICommandHandler这样的抽象,那么中介模式阻止的紧耦合首先就不存在。

中介者模式还封装了复杂的操作,使它们从外部看起来更简单。

return mediator.Send(request);
Run Code Online (Sandbox Code Playgroud)

并不比

return requestHandler.HandleRequest(request);
Run Code Online (Sandbox Code Playgroud)

这两种交互的复杂性是相同的。没有什么是“中介”的。想象一下,您将要在杂货店刷信用卡,然后有人提出要简化您复杂的交互,将您带到另一个收银机,您在那里做完全相同的事情。

CQRS呢?

当涉及到 CQRS 时,中介器是中立的(除非我们有两个独立的中介器,例如ICommandMediatorIQueryMediator。)将我们的命令处理程序与查询处理程序分开,然后注入一个接口,这实际上将它们重新组合在一起并公开所有的接口,这似乎适得其反。我们的命令和查询集中在一处。至少很难说这有助于我们将它们分开。

IMediator用于调用命令和查询处理程序,但与它们隔离的程度无关。如果他们在我们添加调解员之前被隔离,他们仍然是。如果我们的查询处理程序做了一些它不应该做的事情,中介仍然会很高兴地调用它。


我希望这听起来不像是调解员碾过我的狗。但这肯定不是将 CQRS 洒在我们的代码上甚至必然改进我们的架构的灵丹妙药。

我们应该问,有什么好处?它会产生什么不良后果?我是否需要该工具,或者我可以获得我想要的好处而不会产生这些后果?

我断言的是,一旦我们已经依赖于抽象,“隐藏”类的依赖项的进一步步骤通常不会增加任何价值。它们使阅读和理解变得更加困难,并削弱了我们检测和防止其他代码异味的能力。

  • 1. 找到命令/查询的引用,您可以轻松跳转到它。2.通用装饰器是力量所在。如果您想要内置验证/重试/错误处理,只需在通用接口上实现一次即可完成。如果你有所有这些一次性的东西,你就必须手动装饰每一个。无用。3.任何类都可以实现任何接口。说一个类可以实现多个处理程序是一个愚蠢的论点。这就是代码审查的目的。每个命令只知道它包含的数据。 (2认同)

Kon*_*rad 6

部分答案在这里:MediatR何时以及为什么我应该使用它?与 2017 年 webapi 对比

使用 MediaR(或 MicroBus,或任何其他中介器实现)的最大好处是隔离和/或分离您的逻辑(这是使用 CQ RS的流行方式的原因之一)以及实现装饰器模式的良好基础(例如 ASP .NET Core MVC 过滤器)。从 MediatR 3.0 开始,有对此的内置支持(请参阅行为)(而不是使用 IoC 装饰器)

您也可以将装饰器模式与服务(如FooService)一起使用。您也可以将 CQRS 与服务结合使用 ( FooReadService, FooWriteService)

除此之外,它是基于意见的,并使用你想要的来实现你的目标。除了代码维护之外,最终结果不应该有任何区别。

补充阅读: