In the previous code sample, we named the classes after the Mediator pattern actors, as shown in Figure 14.7. While this example is very similar, it uses domain-specific names instead and implements a few more methods to manage the system showing a more tangible implementation. Let’s start with the abstractions:

namespace Mediator;
public interface IChatRoom
{
    void Join(IParticipant participant);
    void Send(ChatMessage message);
}

The IChatRoom interface is the mediator, and it defines two methods instead of one:

  • Join, which allows IParticipant to join IChatRoom.
  • Send, which sends a message to the others.

public interface IParticipant
{
    string Name { get; }
    void Send(string message);
    void ReceiveMessage(ChatMessage message);
    void ChatRoomJoined(IChatRoom chatRoom);
}

The IParticipant interface is the colleague and also has a few more methods:

  • Send, to send messages.
  • ReceiveMessage, to receive messages from the other IParticipant objects.
  • ChatRoomJoined, to confirm that the IParticipant object has successfully joined a chatroom.

public record class ChatMessage(IParticipant Sender, string Content);

The ChatMessage class is the same as the previous Message class, but it references IParticipant instead of IColleague.Let’s now look at the IParticipant implementation:

public class User : IParticipant
{
    private IChatRoom?
_chatRoom;
    private readonly IMessageWriter<ChatMessage> _messageWriter;
    public User(IMessageWriter<ChatMessage> messageWriter, string name)
    {
        _messageWriter = messageWriter ??
throw new ArgumentNullException(nameof(messageWriter));
        Name = name ??
throw new ArgumentNullException(nameof(name));
    }
    public string Name { get; }
    public void ChatRoomJoined(IChatRoom chatRoom)
    {
        _chatRoom = chatRoom;
    }
    public void ReceiveMessage(ChatMessage message)
    {
        _messageWriter.Write(message);
    }
    public void Send(string message)
    {
        if (_chatRoom == null)
        {
            throw new ChatRoomNotJoinedException();
        }
        _chatRoom.Send(new ChatMessage(this, message));
    }
}
public class ChatRoomNotJoinedException : Exception
{
    public ChatRoomNotJoinedException()
        : base(“You must join a chat room before sending a message.”)
    { }
}

The User class represents our default IParticipant. A User instance can chat in only one IChatRoom. THe program can set the chat room by calling the ChatRoomJoined method. When it receives a message, it delegates it to its IMessageWriter<ChatMessage>. Finally, a User instance can send a message through the mediator (IChatRoom). The Send method throws a ChatRoomNotJoinedException to enforce that the User instance must join a chat room before sending messages (code-wise: the _chatRoom field must not be null).We could create a Moderator, Administrator, SystemAlerts, or any other IParticipant implementation as we see fit, but not in this sample. I am leaving that to you to experiment with the Mediator pattern.Now let’s look at the ChatRoom class (the mediator):

public class ChatRoom : IChatRoom
{
    private readonly List<IParticipant> _participants = new();
    public void Join(IParticipant participant)
    {
        _participants.Add(participant);
        participant.ChatRoomJoined(this);
        Send(new ChatMessage(participant, “Has joined the channel”));
    }
    public void Send(ChatMessage message)
    {
        _participants.ForEach(participant
            => participant.ReceiveMessage(message));
    }
}