为什么 Mediator 的 INotification/IRequest 标记接口?

Why INotification/IRequest marker interfaces for Mediator?

虽然我是 MediatR 的忠实粉丝,但将它作为我所有项目的依赖项感觉很奇怪(即使是那些不“调度”并且只是发布 POCO 引用标记接口的项目)。

如果要编写一个类似的中介库,它可以做其他事情但具有共同的 Send/Publish 功能,那么当下面的代码与普通 POCO 一起工作时,对标记接口的需求是什么?

代码通过Send/Publish函数在request/notification之间划定,所以这些标记接口是多余的吗?

如果我要实现自己的调解器,从 NotificationRequest 中删除标记接口是否有任何问题?

它更多的是关于标记接口的设计原则,而不是想改变 MediatR 本身(为了避免冒犯我删除了 MediatR,我重复记录的库很棒,我正在根据自己的目的调整它多线程同步后台基于流的调度因此是设计问题)。

处理程序接口

public interface INotificationHandler<in TNotification> where TNotification : class
{
    Task Handle(TNotification notification, CancellationToken cancellationToken);
}

public interface IRequestHandler<in TRequest, TResponse> where TRequest : class
{
    Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}

没有标记界面的普通 POCO

class TestNotification
{
    public int UserId { get; set; }
    public int Age { get; set; }
}

class TestRequest
{
    public int UserId { get; set; }
}

处理程序

class TestRequestHandler : IRequestHandler<TestRequest, int>
{
    public Task<int> Handle(TestRequest request, CancellationToken cancellationToken)
    {
        return Task.FromResult(request.UserId);
    }
}

class TestNotificationHandler : INotificationHandler<TestNotification>
{
    public Task Handle(TestNotification notification, CancellationToken cancellationToken)
    {
        Console.WriteLine("hello");
        return Task.CompletedTask;
    }
}

主要

static void Main(string[] args)
{
    _ = new TestNotificationHandler()
                .Handle(new TestNotification(), CancellationToken.None);

    var result = new TestRequestHandler()
                        .Handle(new TestRequest() { UserId = 111 }, CancellationToken.None)
                        .Result;

    Console.WriteLine($"result is {result}");
}

C#fiddle

https://dotnetfiddle.net/HTkfh9

1. 当下面的代码与普通 POCO 一起工作时,首先对标记接口的需求是什么?

一起来看看MediatR

namespace MediatR
{
    /// <summary>
    /// Marker interface to represent a request with a void response
    /// </summary>
    public interface IRequest : IRequest<Unit> { }

    /// <summary>
    /// Marker interface to represent a request with a response
    /// </summary>
    /// <typeparam name="TResponse">Response type</typeparam>
    public interface IRequest<out TResponse> : IBaseRequest { }

    /// <summary>
    /// Allows for generic type constraints of objects implementing IRequest or IRequest{TResponse}
    /// </summary>
    public interface IBaseRequest { }
}

namespace MediatR
{
    /// <summary>
    /// Marker interface to represent a notification
    /// </summary>
    public interface INotification { }
}

它们只是空接口,所以我认为 MediatR 将来可能会像这样向 IRequest 添加一些功能:

public interface IRequestType {
    int GetId();

    string GetName();
}
/// <summary>
/// Marker interface to represent a request with a void response
/// </summary>
public interface IRequest : IRequest<Unit> {
    IRequestType GetRequestType();
}

所以我认为你应该实现 IRequestINotification 以避免将来对你的源代码进行大更新

2. Notification 和Request 去掉标记接口有什么问题吗?

目前,对于你的例子来说,还可以 但是当你使用 Publish 功能时,它就不行了

/// <summary>
/// Asynchronously send a notification to multiple handlers
/// </summary>
/// <param name="notification">Notification object</param>
/// <param name="cancellationToken">Optional cancellation token</param>
/// <returns>A task that represents the publish operation.</returns>
Task Publish<TNotification>(TNotification notification, CancellationToken cancellationToken = default)
    where TNotification : INotification;

顺便说一句,我认为 MediatR 需要更新来为 Mediator 创建构建器,如下所示:

IMediator mediator = new Mediator.Builder()
            .AddRequestHandler(typeof(PingRequest), new PingRequestHandler())
            .Build();

你说得对 INotification。如果您要编写自己的 MediatR 实现,则基本上可以删除该标记接口。

但是,对于 IRequest,您需要在请求定义中指定响应类型,以便能够在调用调解器后处理响应。

考虑一个没有定义响应类型的请求:

public class TestRequest
{
}

现在,如果您调用调解器,将无法确定响应类型:

IMediator mediator = GetMediator();

// There is no way to type the response here.
var response = mediator.Send(new TestRequest());

调解员能够键入响应的唯一方法是因为响应定义在请求定义中:

public interface IRequest<out TResponse> : IBaseRequest { }

IMediator Send 定义如下:

Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default);

这使得知道 return 类型成为可能。

所以,长话短说:

  • 你不需要INotification
  • 需要IRequest<TResponse>

其实我刚才也遇到过同样的问题。我真的很想避免让我的所有库都引用 MediatR 包。我也没有看到拥有它们的意义,因为你可以通过将它包装在像这样的通用对象中来绕过它:

public class NotificationWrapper<TNotification> : INotification {
    public TNofication Data { get; }
    public NotificationWrapper(TNotification notification) {
        Data = notification ?? throw new ArgumentNullException(notification);
    }
}

然后使用 INotificationHandler<NotificationWrapper<SomeType>>

的实现来处理它

所以,我最终复制了库并编写了一个没有这些接口的版本。事实证明,您既不需要 INotification 也不需要 IRequest<out TResponse>...但需要权衡。

INotification 遵循 Jesse 上面所说的。但是,对于 IRequest,您会注意到 Send 方法确实有一个用于响应的通用参数。但是,它使用 IRequest<out TResponse> 来推断参数。如果你删除接口,你仍然可以使用 TResponse 来查找处理程序,你只需要明确定义来自调用者的响应类型。在我看来,这是一个很好的权衡,因为如果请求类型多次使用不同的响应类型实现 IRequest<out TResponse>,您无论如何都必须这样做。而且,如果接受您必须明确定义您期望的响应的条件,您可以删除 IRequest<out TResponse> 接口并像这样调用:

IMediator mediator = GetMediator();
mediator.Send<ExpectedResponse>(request);

我这样做的版本托管在这里,并且基于 MediatR 8.0.0: https://github.com/user040F019F/Mediation

诚然,我可能应该做一个 fork/PR,但当时我还不太习惯 github。另外,请忽略自述文件,它是无意义的。