【问题标题】:Replacing service layer with MediatR - is it worth to do it?用 MediatR 替换服务层 - 值得吗?
【发布时间】:2018-11-22 21:36:16
【问题描述】:

您认为将我的服务层或服务类替换为 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 的一个优点是能够轻松插入和拔出逻辑,这似乎非常适合模块化架构,但我仍然有点困惑如何用它来塑造我的架构。

【问题讨论】:

  • 我不确定这与 DDD 有什么关系——最多可能是 CQRS。关于 MediatR 处理程序替换服务,是的,我猜是 way 你会 use it
  • 在这里扮演魔鬼的拥护者是一篇关于为什么在将其带入项目之前需要三思而后行的帖子 - alex-klaus.com/mediator

标签: c# asp.net-core domain-driven-design soa mediatr


【解决方案1】:

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

IRequestHandler&lt;CreateCommand, Response&gt;

更改类以使其依赖于IMediator 有什么好处,

而不是调用

return requestHandler.HandleRequest(request);

它调用

return mediator.Send(request);

结果是我们没有注入我们需要的依赖项,而是注入了一个service locator,这反过来又解决了我们需要的依赖项。

引用 Mark Seeman 的文章,

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

不完全一样

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

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

它使代码更难导航

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

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

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

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

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

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

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

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

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

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

中介者模式

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

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

return mediator.Send(request);

并不比简单

return requestHandler.HandleRequest(request);

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

CQRS 呢?

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

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


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

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

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

【讨论】:

  • 关于此主题的两篇博文:我的“Is Mediator/MediatR still cool?”和 Scott 的“No, MediatR Didn't Run Over My Dog
  • 1.查找对命令/查询的引用,您可以轻松跳转到它。 2. 通用装饰器是强大的地方。如果你想要内置验证/重试/错误处理,你在通用接口上实现一次就完成了。如果你有所有这些,你必须手动装饰每一个。无用。 3.任何类都可以实现任何接口。说一个类可以实现多个处理程序是一个愚蠢的论点。这就是代码审查的目的。每个命令只知道它包含的数据。
  • 装饰器模式在 MediatR 之前就已经存在,可以添加 AOP 装饰器,而无需为每个接口构建单独的类。使用工具做某事的能力并不使其成为该工具独有的功能。 / 每个命令只知道它包含的数据,但是添加对 IMediator 的依赖会有效地添加对每个命令和查询处理程序的依赖。当然,您可以在代码审查中发现违规行为。但我宁愿不引入一个模糊的、命名不准确的依赖项,它可以首先调用任何命令或查询。希望我们能在代码审查中发现这一点。
【解决方案2】:

部分答案在这里:MediatR when and why I should use it? vs 2017 webapi

使用 MediaR(或 MicroBus,或任何其他中介实现)的最大好处是 isolating and/or segregating 您的逻辑(这是使用 CQRS 的流行方式的原因之一)和良好的基础实现decorator pattern(类似于 ASP.NET Core MVC 过滤器)。从 MediatR 3.0 开始,对此提供了内置支持(请参阅 Behaviours)(而不是使用 IoC 装饰器)

您也可以将装饰器模式与服务(FooService 等类)一起使用。您也可以将 CQRS 与服务一起使用(FooReadServiceFooWriteService

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

补充阅读:

【讨论】:

    猜你喜欢
    • 2012-07-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-07
    • 1970-01-01
    • 2021-01-11
    • 2012-05-11
    相关资源
    最近更新 更多