8

您认为用 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);
}

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

目前,我的应用程序根本不是事件驱动的,动态加载的插件也没有简单的通信方式。

我可以将 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)
    {
        ///...
    }
}

会不会做错事?

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

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

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

public CreateRequest : MyDto, IRequest<MyDto> {}        
public UpdateRequest : MyDto, IRequest<MyDto> {} 

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

public CreateRequest : IRequest<MyDto>
{
    MyDto MyDto { get; set; }
}

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

4

2 回答 2

32

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

IRequestHandler<CreateCommand, Response>

改变你的班级有什么好处,它取决于IMediator

而不是打电话

return requestHandler.HandleRequest(request);

它调用

return mediator.Send(request);

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

引用 Mark Seeman 的文章,

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

它不完全一样

var commandHandler = serviceLocator.Resolve<IRequestHandler<CreateCommand, Response>>();
return commandHandler.Handle(request);

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

它使代码更难导航

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

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

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

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

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

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

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

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

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

IMediator不会导致 SRP 违规,并且它的缺失不会阻止它们。但是明确的、特定的依赖关系会引导我们远离此类违规行为。

中介者模式

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

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

return mediator.Send(request);

并不比简单

return requestHandler.HandleRequest(request);

两种交互的复杂性是相同的。没有什么是“中介的”。想象一下,你要在杂货店刷信用卡,然后有人提议通过将你带到另一个收银台来简化你复杂的交互,你在哪里做同样的事情。

那么CQRS呢?

当涉及到 CQRS 时,中介者是中立的(除非我们有两个独立的中介者,比如ICommandMediatorIQueryMediator我们的命令和查询集中在一个地方。至少很难说它有助于我们将它们分开。

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


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

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

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

于 2020-05-29T21:58:29.003 回答
5

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

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

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

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

补充阅读:

于 2018-08-23T12:08:01.803 回答