大部分应用程序通常包含多个组件,这些组件之间通常通过直接引用来进行通讯。但是在某些情况下,并不想某个组件知道其他组件的存在,或者即使知道,也不要通过直接引用的方式来进行通讯或交互,因为这种直接引用的方式就会产生依赖,从而会扩展对象的生命周期,除非通过弱引用的方式来进行。

    中介者模式是一种遍历多个组件之间通讯或交互的模式。他用一个中介对象封装一系列的对象交互,中介者使各对象不需要显示地相互作用,从而使耦合松散,而且可以独立地改变它们之间的交互。

    使用中介模式,对象之间的交互将封装在中介对象中。对象不再直接相互交互(解耦),而是通过中介进行交互。这减少了对象之间的依赖性,从而减少了耦合。

    中介者模式的优点就是减少类间的依赖,把原有的一对多的依赖变成了一对一的依赖,同事类只依赖中介者,减少了依赖,当然同时也降低了类间的耦合;缺点就是中介者会膨胀得很大,而且逻辑复杂,原本N个对象直接的相互依赖关系转换为中介者和同事类的依赖关系,同事类越多,中介者的逻辑就越复杂。

应用场景


    对于关系比较复杂的网状结构,改成点状结构,降低之间的耦合,比如说:

  1. 每一个人都认识很多朋友,大家可以通过微信和朋友交流沟通,微信就是一个中介者。
  2. MVC框架中控制器(C)就是模型(M)和视图(V)的中介者。
  3. 房地产平台与买房者卖房者。

    这里以聊天室的例子来说明,聊天程序是中介者模式的一个很典型的应用。先建一个Person对象来表示一个用户。

class Person
{
    public string Name;
    public ChatRoom chatRoom;
    private List<string> chatLogs;
    public Person(string name)
    {
        Name = name;
    }

    public void Receive(string sender, string msg)
    {
        string s = $"{sender}:'{msg}'";
        Console.WriteLine($"[{Name}'s chat session] {s}");
        chatLogs.Add(s);
    }

    public void Say(string msg)
    {
        chatRoom.BroadCast(Name, msg);
    }

    public void PrivateMessage(string who, string msg)
    {
        chatRoom.Message(Name, who, msg);
    }
}

    Person对象中包含UserName,表示唯一标识;对ChatRoom的引用,来进行发送和接收信息,这是一个中介类。还包含三个方法:

  • Receive方法能够接收信息,将消息显示在屏幕上,并将信息保存到本地的chatLogs中存为历史消息。
  • Say方法能够允许用户将消息广播到聊天室里的所有用户。
  • PrivateMessage方法能够使用户跟特定用户进行聊天。

    Say和PrivateMessage方法需要通过ChatRoom中介类来进行。聊天室对象ChatRoom的实现也很简单:

class ChatRoom
{
    private List<Person> persons = new List<Person>();

    public void Join(Person p)
    {
        string joinMsg = $"{p.Name} join the chat";
        BroadCast(p.Name, joinMsg);
        p.ChatRoom = this;
        persons.Add(p);
    }

    internal void BroadCast(string name, string msg)
    {
        foreach (Person p in persons)
        {
            if (p.Name != name)
            {
                p.Receive(p.Name, msg);
            }
        }
    }

    internal void Message(string name, string who, string msg)
    {
        persons.FirstOrDefault(x => x.Name == name)?.Receive(who, msg);
    }
}

    BroadCast方法,会调用除了发送人自己,所有人的Receive方法来接收信息。Message方法是一对一的聊天,只需要调用待接收人的Receive方法即可,使用方法如下:

var room = new ChatRoom();
var zhangsan = new Person("zhangsan");
var wangwu = new Person("wangwu");
room.Join(zhangsan);
room.Join(wangwu);
zhangsan.Say("hi room");
wangwu.Say("oh,hey zhangsan");
var lisi = new Person("lisi");
room.Join(lisi);
lisi.Say("hello,everyone");
zhangsan.PrivateMessage("lisi", "glad you to join us!");
Console.ReadLine();

    输出结果如下:

[zhangsan's chat session] zhangsan:'wangwu join the chat'
[wangwu's chat session] wangwu:'hi room'
[zhangsan's chat session] zhangsan:'oh,hey zhangsan'
[zhangsan's chat session] zhangsan:'lisi join the chat'
[wangwu's chat session] wangwu:'lisi join the chat'
[zhangsan's chat session] zhangsan:'hello,everyone'
[wangwu's chat session] wangwu:'hello,everyone'
[zhangsan's chat session] lisi:'glad you to join us!'

利用事件实现中介模式


    在前面聊天室的例子中,当有人在聊天室里发布消息后,所有参与者都需要收到提醒,这跟观察者模式非常相像。中介者在所有参与者之间共享事件,参与者通过订阅事件来收到相关通知,也能够发起事件。

    下面再看一个例子,假设在足球运动中,有运动员Player和教练Coach,当教练看到球队得分,教练需要鼓励运动员,并且需要得一些信息比如是谁得分,以及目前球队总得分之类的信息。

    首先定于一下事件参数对象:

abstract class GameEventArgs : EventArgs
{
    public abstract void Print();
}

class PlayerScoredEventArgs : GameEventArgs
{
    public string PlayerName;
    public int GoalsScoreSofar;

    public PlayerScoredEventArgs(string name, int scoreSofar)
    {
        PlayerName = name;
        GoalsScoreSofar = scoreSofar;
    }
    public override void Print()
    {
        Console.WriteLine($"{PlayerName} has scored! their {GoalsScoreSofar} goal");
    }
}

     现在定义中介者对象,因为是事件驱动,所以这里没有行为。

class Game
{
    public EventHandler<GameEventArgs> Events;

    public void Fire(GameEventArgs arg)
    {
        Events?.Invoke(this, arg);
    }
}

     现在构建Player类,它包含名称、得分,以及对Game的引用。

class Player
{
    private string Name;
    private int goalsScore;
    private Game game;

    public Player(string name, Game g)
    {
        Name = name;
        game = g;
    }

    public void Score()
    {
        goalsScore += 1;
        var args = new PlayerScoredEventArgs(Name, goalsScore);
        game.Fire(args);
    }
}

     在Player对象中,当调用player.Score()方法,就会出发PlayerScoredEvent,所有订阅者就会收到消息。现在我们实现教练Coach类。

class Coach
{
    private Game game;

    public Coach(Game g)
    {
        game = g;
        game.Events += (sender, args) => {
            if (args is PlayerScoredEventArgs scored
                     && scored.GoalsScoreSofar < 3)
            {
                Console.WriteLine($"coach says: well done, {scored.PlayerName}");
            }
        };
    }
}

     可以看到,教练引用了Game对象,并且订阅了Game对象的得分事件。当运动员Player得分后,这里就会收到通知,用法如下:

var game = new Game();
var player = new Player("zhangsan", game);
var coach = new Coach(game);

player.Score();
player.Score();
player.Score();

    输出结果如下:

coach says: well done, zhangsan
coach says: well done, zhangsan

    可以看到输出了两条鼓励信息,这里假设运动员得到超过2分之后,教练员不那么兴奋,就不需要鼓励了。

MediatR


    说到中介者设计模式,容易想到MediatR组件,作者Jimmy Bogard,也是开源项目AutoMapper的创建者。MediatR它相当于一个中介者,能够将消息发送者和处理进行分离,非常适合CQRS模式。 

    实际上从 MediatR 源代码中可以看出,它本身也并非标准中介者模式的实现,所以这里简单介绍 MediatR 的其中一种消息传递方式的使用方式:单播消息传递。 

    单播消息传递主要涉及 IRequest(消息类型) 和 IRequestHandler(消息处理) 两个接口。

    现在假设我们的博客系统有一个发表博客的方法,发表博客的参数信息封装在了CreatePostCommand对象中。方法返回的参数为博客的Guid,所以这里需要实现MediatR的IRequest泛型接口,接口泛型类型为Guid,表示请求返回的类型为Guid类型。

[DataContract]
public class CreatePostCommand : IRequest<Guid>
{
    [DataMember]
    public string Content { get; set; }

    [DataMember]
    public string Title { get; set; }

    [DataMember]
    public string Slug { get; set; }

    [DataMember]
    public bool EnableComment { get; set; }

    [DataMember]
    public bool IsPublished { get; set; }

    public CreatePostCommand(string editorContent, string title, string slug, bool enableComment, bool isPublished) 
    {
        Content = editorContent;
        Title = title;
        Slug = slug;
        EnableComment = enableComment;
        IsPublished = isPublished;
    }
}

    然后在另外一个模块中,定义创建博客方法的实际处理类CreatePostCommandHandler,它需要实现对应的IRequestHandler泛型类,泛型参数为请求的对象CreatePostCommand类型以及返回的类型Guid。

 public class CreatePostCommandHandler : IRequestHandler<CreatePostCommand, Guid>
    {
        private readonly IPostRepository _postRepository;
        private readonly IMediator _mediator;
        public CreatePostCommandHandler(IMediator mediator, IPostRepository postRep) 
        {
            _mediator = mediator;
            _postRepository = postRep;
        }

        public async Task<Guid> Handle(CreatePostCommand request, CancellationToken cancellationToken)
        {
            var post = new Post(request.Title.Trim(), request.Slug.ToLower().Trim(), postContent,  request.EnableComment,  request.IsPublished);
            _postRepository.Add(post);
            await _postRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
            return post.Id;
        }
    }

    现在请求对象和处理逻辑已经在MediatR中定义好了。要发起请求,比如我们在Controller中发起发表博客请求,也非常容易,它不需要引用以上实际处理逻辑。只需要往MediatR上发送消息即可。

[Route("api/[controller]")]
[ApiController]
public class PostController : ControllerBase
{
        private readonly IMediator _mediator;
        public PostController(IMediator mediator)
        {
            _mediator = mediator;
        }
  
        [HttpPost]
        public async Task<IActionResult> Post([FromBody]CreatePostCommand command)
        {
            Guid postId = await _mediator.Send(command);
            return Ok(postId);
        }
}

     发起请求的代码逻辑在_mediator.Send()方法里。

     最后,需要通过IOC框架,往系统里注入MeidatR的命令以及处理逻辑即可。使用MediatR可以使得代码的实际请求和处理逻辑分离,两者不需要进行项目引用,各模块只需要与MeidatR进行交互即可,在eShopOnContainer项目中,也用到了MeidiatR。

总结


    中介者设计模式就是关于系统中各个部分之间相互交互的设计模式。

    中介者设计模式最简单的实现,就是定义一个中介者并保存一个所有参与者的列表,然后通过惟一标识号转发各个对象之间的通讯或交互。

    另一种实现就是通过事件的方式,使得系统中的所有参与者可以订阅感兴趣的事件,并且能够触发各自事件,事件携带的参数信息在系统各个部分之间传递。如果某个模块对某个事件不再感兴趣取消订阅即可。

    在本文的最后,介绍了MediatR组件,他类似于一个中介者,又类似于SeviceLocator,能够将消息发送者和处理进行分离,也包含了中介者模式的思想。

 

参考: