用 MediatR 替换服务层——这样做值得吗?
Replacing service layer with MediatR - is it worth to do it?
您认为将我的服务层或服务 类 替换为 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 的一个优点是能够轻松插入和拔出逻辑,这似乎非常适合模块化架构,但我仍然对如何使用它塑造我的架构感到困惑。
这里回答了部分问题:
使用 MediaR(或 MicroBus,或任何其他中介实现)的最大好处是 isolating and/or segregating your logic (one of the reasons its popular way to use CQRS) and a good foundation for implementing decorator pattern (so something like ASP.NET Core MVC filters). From MediatR 3.0 there's an inbuilt support for this (see Behaviours)(而不是使用 IoC 装饰器)
您也可以将装饰器模式用于服务(类,如 FooService
)。您也可以将 CQRS 与服务一起使用 (FooReadService
, FooWriteService
)
除此之外,它是基于意见的,并使用你想要的来实现你的目标。除了代码维护之外,最终结果应该不会有任何不同。
补充阅读:
Baking Round Shaped Apps with MediatR
(将自定义调解器实现与 MediatR 提供的一个和移植过程进行比较)
如果你有一个 class,比方说一个 API 控制器,它取决于
IRequestHandler<CreateCommand, Response>
更改 class 使其依赖于 IMediator
、
有什么好处
而不是调用
return requestHandler.HandleRequest(request);
它调用
return mediator.Send(request);
结果是我们没有注入我们需要的依赖,而是注入了一个 service locator 进而解析了我们需要的依赖。
引用 Mark Seeman 的文章,
In short, the problem with Service Locator is that it hides a class' dependencies, causing run-time errors instead of compile-time errors, as well as making the code more difficult to maintain because it becomes unclear when you would be introducing a breaking change.
和
不完全一样
var commandHandler = serviceLocator.Resolve<IRequestHandler<CreateCommand, Response>>();
return commandHandler.Handle(request);
因为中介仅限于解析命令和查询处理程序,但它很接近。它仍然是一个单一的界面,提供对许多其他界面的访问。
它使代码更难导航
引入IMediator
后,我们的class仍然间接依赖IRequestHandler<CreateCommand, Response>
。不同的是,现在我们不能通过观察来判断。我们无法从接口导航到它的实现。我们可能会推断,如果我们知道要寻找什么——也就是说,如果我们知道命令处理程序接口名称的约定,我们仍然可以遵循依赖关系。但这远不如 class 实际声明它所依赖的东西有用。
当然,我们得到了将接口连接到具体实现而无需编写代码的好处,但节省的时间微不足道,而且由于增加(如果很小)导航困难,我们可能会浪费我们节省的所有时间代码。无论如何,有些库会为我们注册这些依赖项,同时仍然允许我们注入我们实际依赖的抽象。
这是一种依赖于抽象的奇怪、扭曲的方式
有人建议使用调解器来帮助实现装饰器模式。但同样,我们已经通过依赖抽象获得了这种能力。我们可以使用接口的一种实现或添加装饰器的另一种实现。依赖抽象的要点是我们可以在不改变抽象的情况下改变这些实现细节。
详细说明:依赖ISomethingSpecific
的意义在于,我们可以在不修改依赖它的classes的情况下更改或替换实现。但是,如果我们说,“我想更改 ISomethingSpecific
的实现(通过添加装饰器),那么为了实现这一点,我将更改 依赖于的 classes ISomethingSpecific
,它们工作得很好,并使它们依赖于一些通用的、通用的接口”,然后出了问题。还有许多其他方法可以添加装饰器而无需修改不需要更改的代码部分。
是的,使用 IMediator
促进松耦合。但我们已经通过使用明确定义的抽象实现了这一点。在间接层上添加一层并不会增加这种好处。如果您有足够的抽象,可以很容易地编写单元测试,那么您已经受够了。
模糊的依赖关系更容易违反单一职责原则
假设您有一个class下单,它依赖于ICommandHandler<PlaceOrderCommand>
。如果有人试图潜入不属于那里的东西,比如更新用户数据的命令,会发生什么?他们将不得不添加一个新的依赖项,ICommandHandler<ChangeUserAddressCommand>
。如果他们想继续向 class 中添加更多内容,违反 SRP,会发生什么情况?他们将不得不继续添加更多的依赖项。这并不能阻止他们这样做,但至少它照亮了正在发生的事情。
另一方面,如果您可以将各种 运行dom 内容添加到 class 中而不添加更多依赖项会怎么样? class 依赖于可以做 任何事 的抽象。它可以下订单、更新地址、请求销售历史等等,而无需添加任何新的依赖项。如果将 IoC 容器注入它不属于的 class 中,就会遇到同样的问题。它是单个 class 或接口,可用于请求各种依赖项。 这是一个服务定位器。
IMediator
不会导致 SRP 违规,没有它也不会阻止它们。但是明确的、特定的依赖项会引导我们远离此类违规行为。
中介者模式
奇怪的是,使用 MediatR 通常与调解器没有任何关系
图案。中介者模式通过让对象与中介者交互而不是彼此直接交互来促进松散耦合。如果我们已经依赖于像 ICommandHandler
这样的抽象,那么中介模式阻止的紧耦合从一开始就不存在。
中介者模式还封装了复杂的操作,使它们从外部看起来更简单。
return mediator.Send(request);
并不比
简单
return requestHandler.HandleRequest(request);
这两种交互的复杂性是相同的。没有什么是“中介”的。想象一下,您正要在杂货店刷信用卡,然后有人提议通过将您带到另一个收银机来简化您的复杂交互,您可以在那里做完全相同的事情。
CQRS 呢?
当涉及到 CQRS 时,调解器是中立的(除非我们有两个独立的调解器,例如 ICommandMediator
和 IQueryMediator
。)将我们的命令处理程序与查询处理程序分开似乎适得其反,然后注入一个接口,实际上将它们重新组合在一起,并在一个地方公开我们所有的命令和查询。至少很难说它帮助我们将它们分开。
IMediator
用于调用命令和查询处理程序,但与它们隔离的程度无关。如果在我们添加调解器之前它们是隔离的,那么它们现在仍然是。如果我们的查询处理程序做了一些它不应该做的事情,中介仍然会愉快地调用它。
我希望它听起来不像是我狗的调解人 运行。但这肯定不是将 CQRS 洒在我们的代码上,甚至不一定改进我们的架构的灵丹妙药。
我们应该问,有什么好处?它会带来什么不良后果?我是否需要该工具,或者我能否在没有这些后果的情况下获得我想要的好处?
我要断言的是,一旦我们已经依赖于抽象,进一步“隐藏”class 依赖项的步骤通常不会增加任何价值。它们使阅读和理解变得更加困难,并削弱了我们检测和防止其他代码异味的能力。
您认为将我的服务层或服务 类 替换为 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 的一个优点是能够轻松插入和拔出逻辑,这似乎非常适合模块化架构,但我仍然对如何使用它塑造我的架构感到困惑。
这里回答了部分问题:
使用 MediaR(或 MicroBus,或任何其他中介实现)的最大好处是 isolating and/or segregating your logic (one of the reasons its popular way to use CQRS) and a good foundation for implementing decorator pattern (so something like ASP.NET Core MVC filters). From MediatR 3.0 there's an inbuilt support for this (see Behaviours)(而不是使用 IoC 装饰器)
您也可以将装饰器模式用于服务(类,如 FooService
)。您也可以将 CQRS 与服务一起使用 (FooReadService
, FooWriteService
)
除此之外,它是基于意见的,并使用你想要的来实现你的目标。除了代码维护之外,最终结果应该不会有任何不同。
补充阅读:
Baking Round Shaped Apps with MediatR (将自定义调解器实现与 MediatR 提供的一个和移植过程进行比较)
如果你有一个 class,比方说一个 API 控制器,它取决于
IRequestHandler<CreateCommand, Response>
更改 class 使其依赖于 IMediator
、
而不是调用
return requestHandler.HandleRequest(request);
它调用
return mediator.Send(request);
结果是我们没有注入我们需要的依赖,而是注入了一个 service locator 进而解析了我们需要的依赖。
引用 Mark Seeman 的文章,
In short, the problem with Service Locator is that it hides a class' dependencies, causing run-time errors instead of compile-time errors, as well as making the code more difficult to maintain because it becomes unclear when you would be introducing a breaking change.
和
不完全一样var commandHandler = serviceLocator.Resolve<IRequestHandler<CreateCommand, Response>>();
return commandHandler.Handle(request);
因为中介仅限于解析命令和查询处理程序,但它很接近。它仍然是一个单一的界面,提供对许多其他界面的访问。
它使代码更难导航
引入IMediator
后,我们的class仍然间接依赖IRequestHandler<CreateCommand, Response>
。不同的是,现在我们不能通过观察来判断。我们无法从接口导航到它的实现。我们可能会推断,如果我们知道要寻找什么——也就是说,如果我们知道命令处理程序接口名称的约定,我们仍然可以遵循依赖关系。但这远不如 class 实际声明它所依赖的东西有用。
当然,我们得到了将接口连接到具体实现而无需编写代码的好处,但节省的时间微不足道,而且由于增加(如果很小)导航困难,我们可能会浪费我们节省的所有时间代码。无论如何,有些库会为我们注册这些依赖项,同时仍然允许我们注入我们实际依赖的抽象。
这是一种依赖于抽象的奇怪、扭曲的方式
有人建议使用调解器来帮助实现装饰器模式。但同样,我们已经通过依赖抽象获得了这种能力。我们可以使用接口的一种实现或添加装饰器的另一种实现。依赖抽象的要点是我们可以在不改变抽象的情况下改变这些实现细节。
详细说明:依赖ISomethingSpecific
的意义在于,我们可以在不修改依赖它的classes的情况下更改或替换实现。但是,如果我们说,“我想更改 ISomethingSpecific
的实现(通过添加装饰器),那么为了实现这一点,我将更改 依赖于的 classes ISomethingSpecific
,它们工作得很好,并使它们依赖于一些通用的、通用的接口”,然后出了问题。还有许多其他方法可以添加装饰器而无需修改不需要更改的代码部分。
是的,使用 IMediator
促进松耦合。但我们已经通过使用明确定义的抽象实现了这一点。在间接层上添加一层并不会增加这种好处。如果您有足够的抽象,可以很容易地编写单元测试,那么您已经受够了。
模糊的依赖关系更容易违反单一职责原则
假设您有一个class下单,它依赖于ICommandHandler<PlaceOrderCommand>
。如果有人试图潜入不属于那里的东西,比如更新用户数据的命令,会发生什么?他们将不得不添加一个新的依赖项,ICommandHandler<ChangeUserAddressCommand>
。如果他们想继续向 class 中添加更多内容,违反 SRP,会发生什么情况?他们将不得不继续添加更多的依赖项。这并不能阻止他们这样做,但至少它照亮了正在发生的事情。
另一方面,如果您可以将各种 运行dom 内容添加到 class 中而不添加更多依赖项会怎么样? class 依赖于可以做 任何事 的抽象。它可以下订单、更新地址、请求销售历史等等,而无需添加任何新的依赖项。如果将 IoC 容器注入它不属于的 class 中,就会遇到同样的问题。它是单个 class 或接口,可用于请求各种依赖项。 这是一个服务定位器。
IMediator
不会导致 SRP 违规,没有它也不会阻止它们。但是明确的、特定的依赖项会引导我们远离此类违规行为。
中介者模式
奇怪的是,使用 MediatR 通常与调解器没有任何关系
图案。中介者模式通过让对象与中介者交互而不是彼此直接交互来促进松散耦合。如果我们已经依赖于像 ICommandHandler
这样的抽象,那么中介模式阻止的紧耦合从一开始就不存在。
中介者模式还封装了复杂的操作,使它们从外部看起来更简单。
return mediator.Send(request);
并不比
简单return requestHandler.HandleRequest(request);
这两种交互的复杂性是相同的。没有什么是“中介”的。想象一下,您正要在杂货店刷信用卡,然后有人提议通过将您带到另一个收银机来简化您的复杂交互,您可以在那里做完全相同的事情。
CQRS 呢?
当涉及到 CQRS 时,调解器是中立的(除非我们有两个独立的调解器,例如 ICommandMediator
和 IQueryMediator
。)将我们的命令处理程序与查询处理程序分开似乎适得其反,然后注入一个接口,实际上将它们重新组合在一起,并在一个地方公开我们所有的命令和查询。至少很难说它帮助我们将它们分开。
IMediator
用于调用命令和查询处理程序,但与它们隔离的程度无关。如果在我们添加调解器之前它们是隔离的,那么它们现在仍然是。如果我们的查询处理程序做了一些它不应该做的事情,中介仍然会愉快地调用它。
我希望它听起来不像是我狗的调解人 运行。但这肯定不是将 CQRS 洒在我们的代码上,甚至不一定改进我们的架构的灵丹妙药。
我们应该问,有什么好处?它会带来什么不良后果?我是否需要该工具,或者我能否在没有这些后果的情况下获得我想要的好处?
我要断言的是,一旦我们已经依赖于抽象,进一步“隐藏”class 依赖项的步骤通常不会增加任何价值。它们使阅读和理解变得更加困难,并削弱了我们检测和防止其他代码异味的能力。