浅谈Mediator/MediatR

Avoid abusing the Mediator pattern

Posted by eagleboost on September 17, 2022

I’ve been interviewing candidates in the past several months. The candidates are from different sources, both internal and external. In general candidates from Investment Banking industry is better than those from other backgrounds. One of the candidates from Morgan Stanley I interviewed last week even try to teach me a lesson.

Before closing up the interview, as usual I asked the candidate if he has any questions for me. Most of the candidates in this case ask questions related to roles and responsibilities, career development etc. However, the question this candidate (let’s call him S) asked was “during the interview you mentioned that your project uses Dependency Injection, may I ask are the dependencies injected via constructor parameters or properties”. It’s quite a unique question and it surprised me. I told him we use both forms, and personally I prefer property injection because it’s simpler. Constructor injection would require child class to include a constructor to call the base constructor, if inheritance is used, and more parameters need to be added if more dependencies are needed. Given that they generate almost the same thing, constructor injection gives me more troubles than property injection.

S said, yes, too many constructor parameters usually means the design has problems. I agree with that. He continued with “have you heard about the Meditator pattern”? I only have some vague memories about it as I barely use it. He said compare to dependency injection he’d prefer the Mediator pattern and it solves the problem of too many dependencies.

After more talks I got to know that in order to solve the problem of too many dependencies in his project——to save time I didn’t ask why it’s a problem, but it should be because they use constructor dependencies a lot so it becomes headaches——he switch to the Mediator pattern, to be more specific, he used a library called MediatR, and put all dependencies any ViewModel can possibly have into the Mediator, so the ViewModels only depend on the Mediator and the codes are also simpler.

Although I don’t use Mediator in the current project, I quickly sniffed the bad smell of this design from his words. Apparently, on one hand, the dependencies of the ViewModel are not clear anymore, on the other hand, the Mediator may end up holding too many stuff that are totally unrelated.

I gave him an example, consider a ViewModel needs to show message box and input box, so there’re two dependencies, when writing tests, we mock them when we see the properties.

1
2
3
4
5
6
7
8
public class UserManager
{
  [Dependency]
  public IMessageBox MessageBox { get; set; }

  [Dependency]
  public IInputBox InputBox { get; set; }
}

But if we change it to the Mediator way, the usages would be like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class UserManagerWithMediator
{
  [Dependency]
  public IMediator Mediator { get; set; }

  public void ShowMessageBox()
  {
    ...
    Mediator.Send(new MessageBoxData(...));
    ...
  }

  public void ShowInputBox()
  {
    ...
    Mediator.Send(new InputBoxData(...));
    ...
  }
}

For the IMediator interface, there’re several possibilities in terms of the signature of the Send method:

  1. Send method accepts a parameter of type object. It’s a bad design obviously.

  2. Send method accepts a parameter of some sort of interface, e.g. IMediatorContext. It seems more reasonable than object but just worse. First of all, all parameters need to implement this interface before they can be passed, second, because the concept of Mediator is so general, this interface would end up being an empty interface without any member because nothing in common can be abstracted. The IDialogService example I used in Interactions between the View and ViewModel is a comparable bad design. But IDialogService is in a slightly better position because at least it seems information related to a dialog can be abstracted into an interface.

  3. Send method is generic, i.e. Send<T>(...). It seems reasonable but nothing substantially gets better.

In fact no matter which way above is used, the Mediator would inevitably put a lot of unrelated stuff together and becomes a mess quickly. One of the questions I often ask in the interview is how to open a dialog from a ViewModel and get user inputs without breaking the M-V-VM principal, i.e. keep clean separation between the View and the ViewModel. So far the best answer the candidates can give is the solution of InteractionRequest introduced by Prism, not perfect but usable. Most of the candidates can only come up with something similar to IDialogService, which is a solution that almost everybody can work out. Implementation of IDialogService would choose templates based on type of the parameters to create dialogs, so ultimately it would need to handle all kinds of dialogs in the application and would be a total mess.

The mess so far is only about implementations, when it comes to tests, because the dependencies are not clear, writing tests for classes like UserManagerWithMediator would involve more work to do.

Finally S told me that Mediator is so good that he’s PROUD of having chosen it to solve problems, and he would like to show me how to use it in detail if there’s a chance, because it seems I still didn’t quite get how good it is.

After the interview I reviewed the Mediator pattern and the MediatR library. Not surprisingly, I also found quite a few articles talking about “why you shouldn’t use the Mediator pattern”

Below is a definition of quoted from C# Mediator.

The Mediator design pattern defines an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.

A common scenario where the Mediator pattern can be used is the Chat Room, user A and user B can send messages to each other via a Chat Room without directly connecting to each other. The good and bad of the Mediator pattern is out of the scope of this article, but it’s very clear that it’s not intended to solve the problem of too many dependencies or too many constructor parameters. It’s simply a tool for solving certain types of problems. The candidate S just abused it to solve the problem it’s not supposed to solve.

I’ve also read the core part of the MediatR source codes, it uses the third way stated above, aka based on type of T passed to the Send<T>(...) method, it creates an instance of the handler and caches it in a dictionary for future use. The quality is quite amateur level not only the interface design but also the implementations, although it has 8.5k on github. This also tells a truth that average programmers don’t quite care about or understand what good designs are.

I actually have used something similar to the MediatR library in one Android app I wrote this year, the Xamrin MessageCenter. The only difference is that MessageCenter implementation is better.

组里从去年到今年因为各种原因接连走了几个人,最近几个月在密集地面试,希望能把空位填上。候选人来源不一,质量也良莠不齐。总体上来自投行的比来自别的行业的候选人平均水平还是高出一截,前几天一个来自大摩的候选人(下称S)甚至试图给我上一堂课。

细节略去,面试结束后我照例问对方有什么问题想问。一般说来大多数人都围绕职位细节,成长空间等等提问,这位目测应该比我年长一些的S的问题则是“面试中你提到你们的项目使用了依赖注入,我想问是构造函数注入还是属性注入”。第一次遇到类似的问题,老实说挺让我惊喜的。我告诉他都有,以为他想探讨一下各自的优劣,于是说我个人更多使用属性注入,代码更清爽,因为构造函数注入会导致在有继承的情况下子类不得不包含带参数的构造函数来调用基类构造函数,如果依赖增加,参数也会增加,在完成同样工作的情况下构造函数注入没有给我带来任何好处。

S接着说,嗯如果构造函数参数太多那通常说明设计有问题了,我表示同意。他接着说你听过Mediator模式吗?以前学习设计模式的时候我有些印象,但是因为项目中很少用细节也不记得了。他说相比依赖注入我更喜欢用Mediator模式,可以一举解决依赖过多的问题。

谈话从这里开始变得有意思了。原来在他的项目中为了解决多个依赖的问题——时间关系并未深入问为什么这是个并不是问题的问题,估计他碰到的情况是代码过多使用了构造函数依赖导致很头痛——使用了Mediator模式,具体来说用了一个叫做MediatR的类库,把能想到的依赖全都包装到Mediator里面去,从而ViewModel只需要依赖Mediator即可。代码从此变得清爽,世界仿佛也宁静了。

虽然没有使用过Mediator,但听他的叙述我立即发现了其中的问题——如果把依赖都包装进Mediator,那么一方面ViewModel的依赖是什么变得不清晰,另一方面这个Mediator会背负太多的包袱。

我举了一个例子,一个ViewModel需要显示消息框和输入框,因此有两个依赖,那么编写测试代码的时候看到Dependencymock这两个接口即可。

1
2
3
4
5
6
7
8
public class UserManager
{
  [Dependency]
  public IMessageBox MessageBox { get; set; }

  [Dependency]
  public IInputBox InputBox { get; set; }
}

如果使用Mediator模式,那么在调用的时候就是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class UserManagerWithMediator
{
  [Dependency]
  public IMediator Mediator { get; set; }

  public void ShowMessageBox()
  {
    ...
    Mediator.Send(new MessageBoxData(...));
    ...
  }

  public void ShowInputBox()
  {
    ...
    Mediator.Send(new InputBoxData(...));
    ...
  }
}

Mediator来说有几种可能:

  1. Send方法接受一个object类型的参数。这肯定是糟糕的设计。

  2. Send方法接受某个接口作为参数,比如IMediatorContext。看起来比object稍微合理一点,但其实更烂,因为一方面参数必须要实现该接口才行,另一方面由于Mediator的概念过于宽泛,该接口最终只能是一个不包含任何成员的空接口。我在M-V-VM视图交互简述中提到过的IDialogService就是一个可类比的坏设计,不过IDialogService稍微好一点,因为至少(看上去)跟对话框相关的东西可以抽象到一个接口中。

  3. Send方法是泛型的,即Send<T>(...)。看起来更高级一点,但从设计角度来说并没有实质性的提升。

实际上但无论哪种方式,Mediator都会不可避免地会把不相关的东西放在一起,从而变成一个大杂烩。我在面试中常问的一个问题是如何在不违反M-V-VM模式的前提下从ViewModel中打开一个对话框并获取用户输入。目前为止最好的面试者能给出的解决方案是PrismInteractionRequest,可用但不完美。绝大多数面试者则只能达到IDialogService的程度(差不多也是人人都能想到的程度)——用一个类来根据输入参数的类型选择适当的模板来创建对话框,这个类最终会变成一个大杂烩。

说大杂烩指的是实现的问题,此外还有测试,由于依赖不明确,针对UserManagerWithMediator这样的类写测试的话还有额外的工作要做。

S最后还表示Mediator实在是好,他特地用了“自豪”一词来强调他选择这个模式的英明,并表示如果有机会一起工作他会详细演示给我看是怎么用的,因为我似乎没太听懂其好处所在。

我并没有跟他再纠缠下去。结束面试后重温了一下Mediator模式,以及他提到的MediatR类库。毫不意外地也顺手搜到不少类似“为什么不要是使用Mediator模式”的文章,比如这几篇

从定义上说,Mediator模式通过一个类封装一组对象之间的交互,其目标是避免这些对象之间相互引用从而实现松耦合。该定义来自C# Mediator

The Mediator design pattern defines an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.

常见的例子是聊天室,用户A通过聊天室(也就是Mediator)与用户B互相发送消息而不用相互直接关联。Mediator模式的好坏不谈,但显而易见它要解决的问题并不是“依赖注入过多”或者“构造函数注入参数过多”,它只是用来解决某一类问题的工具而已。而候选人S显然是把它滥用了来解决不该它解决的问题。

也浏览了一下MediatR的核心代码,用的是上面说的第三种泛型参数的方法,根据Send<T>(...)传过来的T的类型创建相应的类(因此T不能是接口)并缓存到字典里面供下次调用,不论是接口规范的设计还是实现细节都是很业余的水平,但不妨碍其在github上得到8.5k星,可见一般程序员并不太关心或者不太能理解什么是好的设计。

又想起来我在前段时间写的一个Android小程序还真用过与MediatR类似的东西,那就是Xamrin自带的MessageCenter,不同之处是代码质量高了不少。