MEDIATR 一个低调的中介者类库
2018-04-19

微软官方的开源项目eShopOnContainers中,用到了一个实现中介者模式的类库:MediatR。这个类库的作者叫Jimmy Bogard,在其gtihub主页上可以看到,注明的对象映射组件AutoMapper 就是他写的。其博客上的自我介绍是这么写的:

Headspring的首席架构师,《MVC in Action》的作者,国际演说家,高产的开源软件开发者。擅长分布式系统,REST,消息,领域驱动设计和CQRS。

回到MediatR这个组件,他是一个低调的类库,致力于解决一个简单的问题:解耦进程内消息的发送与处理。跨平台,支持.NET4.5和netstandard1.1。

中介者模式

在继续研究MediatR之前,先回顾下“中介者设计模式(Mediator)”,中介者模式的定义为:用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互应用,从而使其耦合松散,而且可以独立地改变他们之间的交互。其结构图如下:


以下是一个具体的中介者模式demo:

/// <summary>
/// 抽象中介者
/// </summary>
public abstract class AbstractMediator
{
    public abstract void SendMessage(string msg, AbstractColleague colleague);
}

/// <summary>
/// 抽象同事类
/// </summary>
public abstract class AbstractColleague
{
    public string Name { get; set; }
    protected AbstractMediator Mediator;

    protected AbstractColleague(AbstractMediator mediator)
    {
        Mediator = mediator;
    }

    public abstract void PrintMsg(string msg);
}

/// <summary>
/// 具体中介者,负责同事类之间的交互,他必须清楚的知道需要交互的所有同事类的细节。
/// </summary>
public class Mediator : AbstractMediator
{
    public AbstractColleague ColleagueA;
    public AbstractColleague ColleagueB;

    public override void SendMessage(string msg, AbstractColleague colleague)
    {
        if (colleague == ColleagueA)
        {
            ColleagueB.PrintMsg(msg);
        }
        else if (colleague == ColleagueB)
        {
            ColleagueA.PrintMsg(msg);
        }
    }
}

/// <summary>
/// 具体同事类A,他是不知道其他具体同事类的存在的。他与其他同事类的交互,是通过中介者来实现的。
/// </summary>
public class ConcreteColleagueA : AbstractColleague
{
    public ConcreteColleagueA(AbstractMediator mediator) : base(mediator)
    {
    }

    public void SendMessage(string msg)
    {
        Mediator.SendMessage(msg,this);
    }

    public override void PrintMsg(string msg)
    {
        Console.WriteLine($"A收到消息:{msg}");
    }
}

public class ConcreteColleagueB : AbstractColleague
{
    public ConcreteColleagueB(AbstractMediator mediator) : base(mediator)
    {
    }

    public void SendMessage(string msg)
    {
        Mediator.SendMessage(msg, this);
    }

    public override void PrintMsg(string msg)
    {
        Console.WriteLine($"B收到消息:{msg}");
    }
}

class Program
{
    /// <summary>
    /// 客户端调用
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args)
    {
        var mediator = new Mediator();
        var colleagueA = new ConcreteColleagueA(mediator);
        var colleagueB = new ConcreteColleagueB(mediator);
        mediator.ColleagueA = colleagueA;
        mediator.ColleagueB = colleagueB;

        colleagueA.SendMessage("你好B,中午一起饭吧?");
        colleagueB.SendMessage("你好A,好的。");

        Console.ReadLine();
    }
}

程序输出如下:


B收到消息:你好B,中午一起饭吧?
A收到消息:你好A,好的。

中介者类把不同的同事类之间的交互提升到其内部,这样同事类之间的交互变得简单了,同事类不需要知道其他同事类的存在,通过中介者类来完成与其他同事类的交互。另一方面,中介者类本身复杂性增加,中介者类需要知道所有的同事类,例如调用他们的公共方法。

MediatR

MediatR可以与很多依赖注入组件一起工作,其github文档有详细说明。以下是我结合Autofac组件的代码研究。

新建ASP.NET Core Console程序,添加MediatR和Autofac依赖包。然后配置Autofac:

var builder = new ContainerBuilder();
// mediator itself
builder
    .RegisterType<Mediator>()
    .As<IMediator>()
    .InstancePerLifetimeScope();

// request handlers
builder
    .Register<SingleInstanceFactory>(ctx => {
        var c = ctx.Resolve<IComponentContext>();
        return t => c.TryResolve(t, out var o) ? o : null;
    })
    .InstancePerLifetimeScope();

// notification handlers
builder
    .Register<MultiInstanceFactory>(ctx => {
        var c = ctx.Resolve<IComponentContext>();
        return t => (IEnumerable<object>)c.Resolve(typeof(IEnumerable<>).MakeGenericType(t));
    })
    .InstancePerLifetimeScope();

//builder.RegisterType<PingHandler>().AsImplementedInterfaces().InstancePerDependency();
builder.RegisterAssemblyTypes(typeof(Program).GetTypeInfo().Assembly).AsImplementedInterfaces();

var mediator = builder.Build().Resolve<IMediator>();
Test(mediator);

MediatR可以支持几种模式,有请求/响应模式,发布模式。

请求/响应模式,也可以叫做命令模式,主要适用于命令和查询场景。一个请求只能被一个处理者捕获,如果存在多个处理者,那么只有最后一个处理者会被激活。

以下代码声明消息,然后定义处理者:

/*注意:请求/响应接口适用于命令和查询场景。
 *都只能有一个Handler,如果注册多个,只有最后一个会生效。
 */
public class Ping : IRequest<string>
{
    public int MsgId { get; set; }
}

public class PingHandler : IRequestHandler<Ping, string>
{
    public Task<string> Handle(Ping request, CancellationToken cancellationToken)
    {
        return Task.FromResult($"MsgID={request.MsgId},Pong");
    }
}

/// <summary>
/// 为了方便,不需要CancellationToken的Handler,可以继承AsyncRequestHandler类
/// </summary>
public class AsyncNoCancellation : AsyncRequestHandler<Ping, string>
{
    protected override Task<string> HandleCore(Ping request)
    {
        return Task.FromResult("Pong");
    }
}

/// <summary>
/// 如果Handler是完全同步的,可以继承RequestHandler类
/// </summary>
public class SyncHandler : RequestHandler<Ping, string>
{
    protected override string HandleCore(Ping request)
    {
        return $"SyncHandler Pong";
    }
}

然后就是发送请求了:


var response = await mediator.Send(new Ping(){MsgId = 100});
Console.WriteLine(response); // "SyncHandler Pong"

另外,请求/响应模式还支持不带任何返回值的处理者:


public class OneWay:IRequest
{
    public int MsgId { get; set; }
}

public class OneWayHandler : IRequestHandler<OneWay>
{
    public Task Handle(OneWay request, CancellationToken cancellationToken)
    {
        Console.WriteLine($"{request.MsgId},OneWayHandler");
        return Task.CompletedTask;
    }
}

发布模式,一般用于发布一个事件,通知订阅者某件事情已经发生,对此事感兴趣的订阅者可以采取行动了。一般是一个发布这,多个订阅者。


public class Hello : INotification
{
    public int MsgId { get; set; }
}

public class Hello1 : INotificationHandler<Hello>
{
    public async Task Handle(Hello notification, CancellationToken cancellationToken)
    {
        await Task.Delay(3000);
        Console.WriteLine($"{notification.MsgId},{Thread.CurrentThread.ManagedThreadId}");
    }
}

public class Hello2 : INotificationHandler<Hello>
{
    public async Task Handle(Hello notification, CancellationToken cancellationToken)
    {
        await Task.Delay(3000);
        Console.WriteLine($"{notification.MsgId},{Thread.CurrentThread.ManagedThreadId}");
    }
}

像这样发布消息:


  1. await mediator.Publish(new Hello() {MsgId = 300});

程序输出如下:


300,4
300,5
Main 5

这里可以看到,2个订阅者都被激活了。另外,可以看到不同的订阅者所处的线程ID不一样,他们是异步执行的。


原文:http://coderyu.com/2018/04/02/mediatr-%E4%B8%AD%E4%BB%8B%E8%80%85/