Mediating Actions: A Comprehensive Guide to the Mediator Pattern in C#

The Mediator Pattern is a behavioral design pattern that defines an object that encapsulates the communication between a set of objects, known as colleagues. Instead of allowing colleagues to communicate directly, they communicate through the mediator. This pattern promotes loose coupling between objects, making it easier to modify, extend, and maintain the system. In this article, we will explore the Mediator Pattern in-depth, examining its structure, advantages, and providing practical examples in C#.

Understanding the Mediator Pattern

The Mediator Pattern involves the following key components:

  1. Mediator: The interface or abstract class that declares the methods for communication between colleagues. It typically includes methods for colleagues to send and receive messages.
  2. ConcreteMediator: The class that implements the Mediator interface and manages the communication between colleagues. It holds references to all colleagues and coordinates their interactions.
  3. Colleague: The interface or abstract class that declares methods for communication with other colleagues. Colleagues are aware of the mediator but do not have direct references to other colleagues.
  4. ConcreteColleague: The class that implements the Colleague interface and communicates with other colleagues through the mediator. It is unaware of other concrete colleagues.

Implementation in C#

Let's delve into a simple example of the Mediator Pattern in C#. Suppose we want to create a chat application where users can send messages to each other. The Mediator Pattern can be applied to manage the communication between users.

// Step 1: Define Mediator interface
public interface IChatMediator
{
    void SendMessage(string message, IUser sender);
}

// Step 2: Implement ConcreteMediator
public class ChatMediator : IChatMediator
{
    private readonly List<IUser> users;

    public ChatMediator()
    {
        users = new List<IUser>();
    }

    public void AddUser(IUser user)
    {
        users.Add(user);
    }

    public void SendMessage(string message, IUser sender)
    {
        foreach (var user in users)
        {
            // Exclude the sender from receiving the message
            if (user != sender)
            {
                user.ReceiveMessage(message);
            }
        }
    }
}

// Step 3: Define Colleague interface
public interface IUser
{
    void SendMessage(string message);
    void ReceiveMessage(string message);
}

// Step 4: Implement ConcreteColleague
public class ChatUser : IUser
{
    private readonly string name;
    private readonly IChatMediator mediator;

    public ChatUser(string name, IChatMediator mediator)
    {
        this.name = name;
        this.mediator = mediator;
    }

    public void SendMessage(string message)
    {
        Console.WriteLine($"{name} sends message: {message}");
        mediator.SendMessage(message, this);
    }

    public void ReceiveMessage(string message)
    {
        Console.WriteLine($"{name} receives message: {message}");
    }
}

// Step 5: Client code
public class Client
{
    public void Run()
    {
        IChatMediator chatMediator = new ChatMediator();

        IUser user1 = new ChatUser("User 1", chatMediator);
        IUser user2 = new ChatUser("User 2", chatMediator);
        IUser user3 = new ChatUser("User 3", chatMediator);

        chatMediator.AddUser(user1);
        chatMediator.AddUser(user2);
        chatMediator.AddUser(user3);

        user1.SendMessage("Hello, everyone!");
    }
}

In this example, IChatMediator is the mediator interface that declares the SendMessage method. ChatMediator is the concrete mediator class that implements the IChatMediator interface and manages the communication between users. IUser is the colleague interface that declares the SendMessage and ReceiveMessage methods. ChatUser is the concrete colleague class that implements the IUser interface and communicates with other users through the mediator.

Advantages of the Mediator Pattern

1. Decoupling: The Mediator Pattern promotes loose coupling between objects by eliminating direct references between them. This makes it easier to modify and extend the system.

2. Centralized Control: The pattern provides a centralized control point for communication, allowing better coordination and management of interactions.

3. Reusability: Colleagues can be reused in different scenarios as they are unaware of each other's existence. Adding new colleagues is also straightforward.

4. Simplification of Communication: Colleagues communicate through a mediator, simplifying the communication process and avoiding complex interconnections.

Real-world Examples

1. Air Traffic Control System

In an air traffic control system, aircraft communicate with each other through a central air traffic control (ATC) system. The ATC system acts as a mediator, facilitating communication between aircraft to avoid collisions and ensure safe navigation.

// Simplified example in C#
public interface IAirTrafficControl
{
    void RegisterAircraft(Aircraft aircraft);
    void SendMessage(string message, Aircraft sender);
}

public class Aircraft
{
    private readonly string callsign;
    private readonly IAirTrafficControl atc;

    public Aircraft(string callsign, IAirTrafficControl atc)
    {
        this.callsign = callsign;
        this.atc = atc;
        atc.RegisterAircraft(this);
    }

    public void SendMessage(string message)
    {
        Console.WriteLine($"{callsign} sends message: {message}");
        atc.SendMessage(message, this);
    }

    public void ReceiveMessage(string message)
    {
        Console.WriteLine($"{callsign} receives message: {message}");
    }
}

In this example, IAirTrafficControl is the mediator interface that declares the RegisterAircraft and SendMessage methods. The Aircraft class is a colleague that registers with the ATC system and communicates with other aircraft through the mediator.

2. Stock Trading System

In a stock trading system, different stock brokers and traders may need to communicate to execute trades and update market information. The Mediator Pattern can be applied to manage this communication.

// Simplified example in C#
public interface IStockExchange
{
    void RegisterParticipant(IParticipant participant);
    void BroadcastMessage(string message, IParticipant sender);
}

public interface IParticipant
{
    void SendMessage(string message);
    void ReceiveMessage(string message);
}

public class StockBroker : IParticipant
{
    private readonly string name;
    private readonly IStockExchange stockExchange;

    public StockBroker(string name, IStockExchange stockExchange)
    {
        this.name = name;
        this.stockExchange = stockExchange;
        stockExchange.RegisterParticipant(this);
    }

    public void SendMessage(string message)
    {
        Console.WriteLine($"{name} sends message: {message}");
        stockExchange.BroadcastMessage(message, this);
    }

    public void ReceiveMessage(string message)
    {
        Console.WriteLine($"{name} receives message: {message}");
    }
}

In this example, IStockExchange is the mediator interface that declares the RegisterParticipant and BroadcastMessage methods. IParticipant is the colleague interface that declares the SendMessage and ReceiveMessage methods. StockBroker is a concrete colleague that registers with the stock exchange and communicates with other participants through the mediator.

Conclusion

The Mediator Pattern is a valuable tool for managing communication between objects in a system, promoting loose coupling and flexibility. Through practical examples in C#, we have demonstrated how the Mediator Pattern can be applied to real-world scenarios, providing a blueprint for creating systems that involve the coordination of multiple objects. Understanding and incorporating this pattern into your design practices can contribute to building modular, extensible, and maintainable software architectures, ensuring efficient communication between components in your applications.