© Dmitri Nesteruk 2020
D. NesterukDesign Patterns in .NET Core 3https://doi.org/10.1007/978-1-4842-6180-4_18

18. Mediator

Dmitri Nesteruk1  
(1)
St. Petersburg, c.St-Petersburg, Russia
 

A large proportion of the code we write has different components (classes) communicating with one another through direct references. However, there are situations when you don’t want objects to be necessarily aware of each other’s presence. Or, perhaps you do want them to be aware of one another, but you still don’t want them to communicate through references, because as soon as you keep and hold a reference to something, you extend that object’s lifetime beyond what might originally be desired (unless it’s a WeakReference, of course).

So the Mediator is a mechanism for facilitating communication between the components. Naturally, the mediator itself needs to be accessible to every component taking part, which means it should either be a publicly available static variable or, alternatively, just a reference that gets injected into every component.

Chat Room

Your typical Internet chat room is the classic example of the Mediator design pattern, so let’s implement this before we move on to the more complicated stuff.

The most trivial implementation of a participant in a chat room can be as simple as
public class Person
{
  public string Name;
  public ChatRoom Room;
  private List<string> chatLog = new List<string>();
  public Person(string name) => Name = name;
  public void Receive(string sender, string message)
  {
    string s = $"{sender}: '{message}'";
     WriteLine($"[{Name}'s chat session] {s}");
     chatLog.Add(s);
  }
  public void Say(string message) => Room.Broadcast(Name, message);
  public void PrivateMessage(string who, string message)
  {
    Room.Message(Name, who, message);
  }
}
So we’ve got a person with a Name (user ID), a chat log, and a reference to the actual ChatRoom . We have a constructor and then three methods:
  • Receive() allows us to receive a message. Typically, what this function would do is show the message on the user’s screen, and also add it to the chat log.

  • Say() allows the person to broadcast a message to everyone in the room.

  • PrivateMessage() is private messaging functionality. You need to specify the name of the person the message is intended for.

Both Say() and PrivateMessage() 1 just relay operations to the chat room. Speaking of which, let’s actually implement ChatRoom – it’s not particularly complicated.
public class ChatRoom
{
  private List<Person> people = new List<Person>();
  public void Broadcast(string source, string message) { ... }
  public void Join(Person p) { ... }
  public void Message(string source, string destination,
    string message) { ... }
}
So, I have decided to go with pointers here. The ChatRoom API is very simple:
  • Join() gets a person to join the room. We are not going to implement Leave(), instead deferring the idea to a subsequent example in this chapter.

  • Broadcast() sends the message to everyone… well, not quite everyone: we don’t need to send the message back to the person that sent it.

  • Message() sends a private message.

The implementation of Join() is as follows:
public void Join(Person p)
{
  string joinMsg = $"{p.Name} joins the chat";
  Broadcast("room", joinMsg);
  p.Room = this;
  people.Add(p);
}

Just like a classic IRC chat room, we broadcast the message that someone has joined to everyone in the room. The first argument of Broadcast() , the origin parameter , in this case, is specified as “room” rather than the person that’s joined. We then set the person’s room reference and add them to the list of people in the room.

Now, let’s look at Broadcast(): this is where a message is sent to every room participant. Remember, each participant has its own Person.Receive() method for processing the message, so the implementation is somewhat trivial:
public void Broadcast(string source, string message)
{
  foreach (var p in people)
    if (p.Name != source)
      p.Receive(source, message);
}

Whether or not we want to prevent a broadcast message to be relayed to ourselves is a point of debate, but I’m actively avoiding it here. Everyone else gets the message, though.

Finally, here’s private messaging implemented with Message():
public void Message(string source, string destination, string message)
{
  people.FirstOrDefault(p => p.Name == destination)
    ?.Receive(source, message);
}

This searches for the recipient in the list of people and, if the recipient is found (because who knows, they could have left the room), dispatches the message to that person.

Coming back to Person’s implementations of Say() and PrivateMessage() , here they are:
public void Say(string message) => Room.Broadcast(Name, message);
public void PrivateMessage(string who, string message)
{
  Room.Message(Name, who, message);
}
As for Receive() , well, this is a good place to actually display the message on-screen as well as add it to the chat log:
public void Receive(string sender, string message)
{
  string s = $"{sender}: '{message}'";
  WriteLine($"[{Name}'s chat session] {s}");
  chatLog.Add(s);
}

We go the extra mile here by displaying not just who the message came from, but whose chat session we’re currently in – this will be useful for diagnosing who said what and when.

Here’s the scenario that we’ll run through:
var room = new ChatRoom();
var john = new Person("John");
var jane = new Person("Jane");
room.Join(john);
room.Join(jane);
john.Say("hi room");
jane.Say("oh, hey john");
var simon = new Person("Simon");
room.Join(simon);
simon.Say("hi everyone!");
jane.PrivateMessage("Simon", "glad you could join us!");
Here is the output:
[john's chat session] room: "jane joins the chat"
[jane's chat session] john: "hi room"
[john's chat session] jane: "oh, hey john"
[john's chat session] room: "simon joins the chat"
[jane's chat session] room: "simon joins the chat"
[john's chat session] simon: "hi everyone!"
[jane's chat session] simon: "hi everyone!"
[simon's chat session] jane: "glad you could join us, simon"

And here’s an illustration of the chat room operations :

../images/476082_2_En_18_Chapter/476082_2_En_18_Figa_HTML.jpg

Mediator with Events

In the chat room example, we’ve encountered a consistent theme: the participants need notification whenever someone posts a message. This seems like a perfect scenario for the Observer pattern, which is discussed later in the book: the idea of the mediator having an event that is shared by all participants; participants can then subscribe to the event to receive notifications, and they can also cause the event to fire, thus triggering said notifications.

Instead of redoing the chat room once again, let’s go for a simpler example: imagine a game of football (soccer for my readers in the United States) with players and a football coach. When the coach sees their team scoring, they naturally want to congratulate the player. Of course, they need some information about the event, like who scored the goal and how many goals they have scored so far.

We can introduce a base class for any sort of event data:
abstract class GameEventArgs : EventArgs
{
  public abstract void Print();
}
I’ve added the Print() deliberately to print the event’s contents to the command line. Now, we can derive from this class in order to store some goal-related data:
class PlayerScoredEventArgs : GameEventArgs
{
  public string PlayerName;
  public int GoalsScoredSoFar;
  public PlayerScoredEventArgs
    (string playerName, int goalsScoredSoFar)
  {
    PlayerName = playerName;
    GoalsScoredSoFar = goalsScoredSoFar;
  }
  public override void Print()
  {
    WriteLine($"{PlayerName} has scored! " +
              $"(their {GoalsScoredSoFar} goal)");
  }
}
We are once again going to build a mediator, but it will have no behaviors! Seriously, with an event-driven infrastructure, they are no longer needed:
class Game
{
  public event EventHandler<GameEventArgs> Events;
  public void Fire(GameEventArgs args)
  {
    Events?.Invoke(this, args);
  }
}

As you can see, we’ve just made a central place where all game events are being generated. The generation itself is polymorphic: the event uses a GameEventArgs type , and you can test the argument against the various types available in your application. The Fire() utility method just helps us safely raise the event.

We can now construct the Player class. A player has a name, the number of goals they scored during the match, and a reference to the mediator Game, of course:
class Player
{
  private string name;
  private int goalsScored = 0;
  private Game game;
  public Player(Game game, string name)
  {
    this.name = name;
    this.game = game;
  }
  public void Score()
  {
    goalsScored++;
    var args = new PlayerScoredEventArgs(name, goalsScored);
    game.Fire(args);
  }
}
The Player.Score() method is where we make PlayerScoredEventArgs and post them for all subscribers to see. Who gets this event? Why, a Coach, of course:
class Coach
{
  private Game game;
  public Coach(Game game)
  {
    this.game = game;
    // celebrate if player has scored <3 goals
    game.Events += (sender, args) =>
    {
      if (args is PlayerScoredEventArgs scored
          && scored.GoalsScoredSoFar < 3)
      {
        WriteLine($"coach says: well done, {scored.PlayerName}");
      }
    };
  }
}

The implementation of the Coach class is trivial; our coach doesn’t even get a name. But we do give him a constructor where a subscription is created to game’s Events such that whenever something happens, the coach gets to process the event data in the provided lambda.

Notice that the argument type of the lambda is GameEventArgs  – we don’t know if a player has scored or has been sent off, so we need a cast to determine we’ve got the right type.

The interesting thing is that all the magic happens at the setup stage: there’s no need to explicitly subscribe to particular events. The client is free to create objects using their constructors, and then when the player scores, the notifications are sent:
var game = new Game();
var player = new Player(game, "Sam");
var coach = new Coach(game);
player.Score(); // coach says: well done, Sam
player.Score(); // coach says: well done, Sam
player.Score(); //

The output is only two lines long because, on the third goal, the coach isn’t impressed anymore.

Introduction to MediatR

MediatR is one of a number of libraries written to provide a shrink-wrapped Mediator implementation in .NET.2 It provides the client a central Mediator component, as well as interfaces for requests and request handlers. It supports both synchronous and async/await paradigms and provides support for both directed messages and broadcasting.

As you may have guessed, MediatR is designed to work with an IoC container. It comes with examples for how to get it running with most popular containers out there; I’ll be using Autofac for my examples.

The first steps are general: we simply set up MediatR under our IoC container and also register our own types through the interfaces they implement.
var builder = new ContainerBuilder();
builder.RegisterType<Mediator>()
  .As<IMediator>()
  .InstancePerLifetimeScope(); // singleton
builder.Register<ServiceFactory>(context =>
{
  var c = context.Resolve<IComponentContext>();
  return t => c.Resolve(t);
});
builder.RegisterAssemblyTypes(typeof(Demo).Assembly)
  .AsImplementedInterfaces();

The central Mediator , which we registered as a singleton, is in charge of routing requests to request handlers and getting responses from them. Each request is expected to implement the IRequest<T> interface, where T is the type of the response that is expected for this request. If there is no data to return, you can use a nongeneric IRequest instead.

Here’s a simple example:
public class PingCommand : IRequest<PongResponse> {}
So in our trivial demo, we intend to send a PingCommand and receive a PongResponse. The response doesn’t have to implement any interface; we’ll define it like this:
public class PongResponse
{
  public DateTime Timestamp;
  public PongResponse(DateTime timestamp)
  {
    Timestamp = timestamp;
  }
}
The glue that connects requests and responses together is MediatR’s IRequestHandler interface. It has a single member called Handle that takes a request and a cancellation token and returns the result of the call:
[UsedImplicitly]
public class PingCommandHandler
  : IRequestHandler<PingCommand, PongResponse>
{
  public async Task<PongResponse> Handle(PingCommand request,
    CancellationToken cancellationToken)
  {
    return await Task
      .FromResult(new PongResponse(DateTime.UtcNow))
      .ConfigureAwait(false);
  }
}

Note the use of the async/await paradigm earlier, with the Handle method returning a Task<T>. If you don’t actually need your request to produce a response, then instead of using an IRequestHandler , you can use the AsyncRequestHandler base class, whose Handle() method returns a humble nongeneric Task. Oh, and in case your request is synchronous, you can inherit from RequestHandler<TRequest, TResponse> class instead.

This is all that you need to do to actually set up two components and get them talking through the central mediator. Note that the mediator itself does not feature in any of the classes we’ve created: it works behind the scenes.

Putting everything together, we can use our setup as follows:
var container = builder.Build();
var mediator = container.Resolve<IMediator>();
var response = await mediator.Send(new PingCommand());
Console.WriteLine($"We got a pong at {response.Timestamp}");
You’ll notice that request/response messages are targeted: they are dispatched to a single handler. MediatR also supports notification messages, which can be dispatched to multiple handlers. In this case, your request needs to implement the INotification interface:
public class Ping : INotification {}
And now you can create any number of INotification<Ping> classes that get to process these notifications :
public class Pong : INotificationHandler<Ping>
{
    public Task Handle(Ping notification,
                       CancellationToken cancellationToken)
    {
        Console.WriteLine("Got a ping");
        return Task.CompletedTask;
    }
}
public class AlsoPong : INotificationHandler<Ping> { ... }
For notifications, instead of using the Send() method, we use the Publish() method:
await mediator.Publish(new Ping());

There is more information about MediatR available on its official Wiki page (https://github.com/jbogard/MediatR).

Summary

The Mediator design pattern is all about having an in-between component that everyone in a system has a reference to and can use to communicate with one another. Instead of direct references, communication can happen through identifiers (usernames, unique IDs, GUIDs, etc.).

The simplest implementation of a mediator is a member list and a function that goes through the list and does what it’s intended to do – whether on every element of the list, or selectively.

A more sophisticated implementation of Mediator can use events to allow participants to subscribe (and unsubscribe) to things happening in the system. This way, messages sent from one component to another can be treated as events. In this setup, it is also easy for participants to unsubscribe to certain events if they are no longer interested in them or if they are about to leave the system altogether.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.133.96