Implement the Mediator Pattern in C

The Mediator pattern is a behavioral design pattern that reduces chaotic dependencies between objects by restricting direct communication and forcing them to collaborate through a central mediator object. In this article, you will learn the core concepts of the Mediator pattern, walk through a step-by-step implementation in C# using pure object-oriented principles, and see how this approach decouples individual classes to improve testability and maintainability.

The Problem: Tight Coupling Between Components

When multiple classes interact directly, they form a tightly coupled web of dependencies. For instance, in a user interface dialog, a checkbox might need to update a text field, toggle a submit button, and clear a label. If each component directly references every other component, changing one class risks breaking others, and reusing a single component elsewhere becomes nearly impossible.

The Mediator pattern solves this by making individual components (colleagues) interact only with an intermediary interface rather than referencing each other directly.


Key Components of the Mediator Pattern

  1. Mediator Interface: Defines methods for communication between colleague components.
  2. Concrete Mediator: Implements the mediator interface and coordinates interaction between colleague objects.
  3. Colleague Classes: Individual classes that contain business logic and communicate exclusively through the mediator.

Step-by-Step Implementation in C

The following example demonstrates a form dialog where UI elements (colleagues) communicate changes through a dialog mediator.

Step 1: Define the Mediator Interface

public interface IMediator
{
    void Notify(object sender, string eventCode);
}

Step 2: Define the Base Colleague

Colleague classes keep a reference to the mediator instance to send notifications when their state changes.

public abstract class Component
{
    protected IMediator Mediator;

    public Component(IMediator mediator = null)
    {
        Mediator = mediator;
    }

    public void SetMediator(IMediator mediator)
    {
        Mediator = mediator;
    }
}

Step 3: Implement Concrete Colleague Classes

These components contain specific behavior. They notify the mediator when an action occurs, rather than calling other components directly.

public class Button : Component
{
    public bool IsEnabled { get; private set; } = false;

    public void SetEnabled(bool enabled)
    {
        IsEnabled = enabled;
        Console.WriteLine($"Button enabled status: {IsEnabled}");
    }

    public void Click()
    {
        Console.WriteLine("Button clicked.");
        Mediator.Notify(this, "ButtonClick");
    }
}

public class TextBox : Component
{
    public string Text { get; private set; } = string.Empty;

    public void EnterText(string text)
    {
        Text = text;
        Console.WriteLine($"TextBox text changed to: '{Text}'");
        Mediator.Notify(this, "TextChanged");
    }
}

public class CheckBox : Component
{
    public bool IsChecked { get; private set; }

    public void Check()
    {
        IsChecked = !IsChecked;
        Console.WriteLine($"CheckBox toggled to: {IsChecked}");
        Mediator.Notify(this, "CheckboxToggled");
    }
}

Step 4: Implement the Concrete Mediator

The concrete mediator holds references to all components and encapsulates the conditional logic that determines how they respond to events.

public class RegistrationDialogMediator : IMediator
{
    private readonly TextBox _usernameTextBox;
    private readonly CheckBox _termsCheckBox;
    private readonly Button _submitButton;

    public RegistrationDialogMediator(TextBox textBox, CheckBox checkBox, Button button)
    {
        _usernameTextBox = textBox;
        _usernameTextBox.SetMediator(this);

        _termsCheckBox = checkBox;
        _termsCheckBox.SetMediator(this);

        _submitButton = button;
        _submitButton.SetMediator(this);
    }

    public void Notify(object sender, string eventCode)
    {
        switch (eventCode)
        {
            case "TextChanged":
            case "CheckboxToggled":
                ValidateForm();
                break;

            case "ButtonClick":
                SaveData();
                break;
        }
    }

    private void ValidateForm()
    {
        bool isValid = !string.IsNullOrWhiteSpace(_usernameTextBox.Text) && _termsCheckBox.IsChecked;
        _submitButton.SetEnabled(isValid);
    }

    private void SaveData()
    {
        Console.WriteLine("Registration data saved successfully.");
    }
}

Step 5: Execute the Pattern

class Program
{
    static void Main()
    {
        var textBox = new TextBox();
        var checkBox = new CheckBox();
        var button = new Button();

        // The mediator registers and binds the components
        var mediator = new RegistrationDialogMediator(textBox, checkBox, button);

        // Simulate user input
        textBox.EnterText("john_doe"); // Checkbox is still false -> Button remains disabled
        checkBox.Check();              // Checkbox true and text present -> Button becomes enabled
        button.Click();                // Triggers form submission via mediator
    }
}

Benefits of Using the Mediator Pattern

  • Single Responsibility Principle: Communication rules are centralized inside the mediator rather than scattered across various colleague classes.
  • Open/Closed Principle: New mediators can be introduced without modifying existing colleague components.
  • Decoupled Architecture: Individual components can be isolated and unit tested without instantiating the entire network of related objects.