Understanding the Mediator Design Pattern in C#

Joshua AkosaJoshua Akosa
4 min read

In software development, as systems grow more complex, objects start interacting with each other in increasingly intricate ways. Over time, this can lead to a tightly coupled, hard-to-maintain system where modifying or extending a particular feature becomes a nightmare. The Mediator Design Pattern is a great solution to untangle these dependencies and keep objects loosely coupled by centralizing communication through a mediator object.

In this blog, we’ll dive deep into the Mediator pattern, explore its application in C#, and see how it promotes cleaner, more modular code.


What is the Mediator Pattern?

The Mediator Pattern is a behavioral design pattern that facilitates communication between objects by centralizing interactions into a single mediator object. Instead of objects communicating directly, they send messages to the mediator, which in turn forwards the messages to the appropriate objects.

In essence, it reduces the direct dependencies between classes, simplifying the code structure and enhancing maintainability.

Key Characteristics of the Mediator Pattern:
  • Centralized Communication: All communication between components is managed by a mediator, preventing direct interaction between them.

  • Loose Coupling: Objects no longer depend on each other, making the system easier to modify or extend.

  • Single Responsibility: The mediator assumes the responsibility of coordinating communication, allowing individual components to focus solely on their tasks.


Implementing the Mediator Pattern in C

Let's look at a real-world analogy: imagine a chat room where users send messages to each other. Instead of each user communicating directly, they communicate through a central chat room server (the mediator), which ensures that messages are forwarded appropriately.

Here’s how you can implement this in C# using the Mediator pattern:

Step 1: Define a Mediator Interface

First, we define a mediator interface to outline the communication method:

public interface IChatMediator
{
    void SendMessage(string message, User user);
    void RegisterUser(User user);
}

Step 2: Create the Concrete Mediator

Next, we implement the mediator interface to centralize message passing between users:

using System.Collections.Generic;

public class ChatMediator : IChatMediator
{
    private List<User> users = new List<User>();

    public void RegisterUser(User user)
    {
        users.Add(user);
    }

    public void SendMessage(string message, User user)
    {
        foreach (var u in users)
        {
            // Do not send the message to the user who sent it
            if (u != user)
            {
                u.ReceiveMessage(message);
            }
        }
    }
}

The ChatMediator keeps track of all registered users and forwards messages to everyone except the sender.

Step 3: Define a User Class

Now we create a User class that will communicate through the mediator:

public abstract class User
{
    protected IChatMediator mediator;
    protected string name;

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

    public abstract void SendMessage(string message);
    public abstract void ReceiveMessage(string message);
}

Here, User is an abstract class that forces any derived class to implement methods to send and receive messages.

Step 4: Create Concrete User Classes

Let’s create specific user classes that implement the User class:

public class ConcreteUser : User
{
    public ConcreteUser(IChatMediator mediator, string name) : base(mediator, name) {}

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

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

Step 5: Demonstration

Now, let’s set up a scenario where users communicate through the ChatMediator:

class Program
{
    static void Main(string[] args)
    {
        IChatMediator chatMediator = new ChatMediator();

        User user1 = new ConcreteUser(chatMediator, "Alice");
        User user2 = new ConcreteUser(chatMediator, "Bob");
        User user3 = new ConcreteUser(chatMediator, "Charlie");

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

        user1.SendMessage("Hello everyone!");
        user2.SendMessage("Hi Alice!");
    }
}

Output:

Alice sends: Hello everyone!
Bob receives: Hello everyone!
Charlie receives: Hello everyone!

Bob sends: Hi Alice!
Alice receives: Hi Alice!
Charlie receives: Hi Alice!

In this scenario, Alice sends a message to the chat room (mediator), which forwards it to Bob and Charlie. Similarly, when Bob sends a message, it’s forwarded to the other users.


Benefits of Using the Mediator Pattern

  1. Reduced Coupling
    The most significant benefit of the Mediator pattern is the reduction of dependencies between communicating objects. Instead of objects knowing each other directly, they only know the mediator, making the system more modular and easier to maintain.

  2. Enhanced Maintainability
    Since communication is centralized, modifying how objects interact or introducing new objects becomes easier. There’s no need to change existing classes, just the mediator itself.

  3. Single Point of Communication
    Mediators provide a single location where the interaction logic resides, which can simplify debugging and testing, as there’s one central place to control communication flow.


When to Use the Mediator Pattern?

  • When there is complex communication between a set of objects that would result in tangled, hard-to-manage dependencies.

  • When you want to encapsulate how objects interact in a central place without making those objects aware of each other.

  • When introducing new communication paths would otherwise involve modifying many classes.


Conclusion

The Mediator pattern is a powerful tool for managing complex interactions in a loosely coupled way, promoting cleaner, more maintainable code. In C#, it allows you to centralize communication between objects, preventing direct dependencies between them and simplifying your system's architecture.

By applying the Mediator pattern, you can turn a tangled web of object interactions into a clean, centralized communication hub that scales with your application’s complexity.

10
Subscribe to my newsletter

Read articles from Joshua Akosa directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Joshua Akosa
Joshua Akosa