大部分应用程序通常包含多个组件,这些组件之间通常通过直接引用来进行通讯。但是在某些情况下,并不想某个组件知道其他组件的存在,或者即使知道,也不要通过直接引用的方式来进行通讯或交互,因为这种直接引用的方式就会产生依赖,从而会扩展对象的生命周期,除非通过弱引用的方式来进行。
中介者模式是一种遍历多个组件之间通讯或交互的模式。他用一个中介对象封装一系列的对象交互,中介者使各对象不需要显示地相互作用,从而使耦合松散,而且可以独立地改变它们之间的交互。
使用中介模式,对象之间的交互将封装在中介对象中。对象不再直接相互交互(解耦),而是通过中介进行交互。这减少了对象之间的依赖性,从而减少了耦合。
中介者模式的优点就是减少类间的依赖,把原有的一对多的依赖变成了一对一的依赖,同事类只依赖中介者,减少了依赖,当然同时也降低了类间的耦合;缺点就是中介者会膨胀得很大,而且逻辑复杂,原本N个对象直接的相互依赖关系转换为中介者和同事类的依赖关系,同事类越多,中介者的逻辑就越复杂。
应用场景
对于关系比较复杂的网状结构,改成点状结构,降低之间的耦合,比如说:
- 每一个人都认识很多朋友,大家可以通过微信和朋友交流沟通,微信就是一个中介者。
- MVC框架中控制器(C)就是模型(M)和视图(V)的中介者。
- 房地产平台与买房者卖房者。
这里以聊天室的例子来说明,聊天程序是中介者模式的一个很典型的应用。先建一个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,能够将消息发送者和处理进行分离,也包含了中介者模式的思想。
参考: