本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:中介者模式是一种行为型设计模式,用于降低多个对象之间的耦合度,将复杂的网状交互关系转化为以中介者为中心的星形结构。通过引入中介者对象协调同事类之间的通信,系统更易于扩展和维护。本文基于C#语言,通过TestMediator和TestMediaForm类展示了中介者模式的具体实现,涵盖接口定义、消息传递机制及类间解耦方法,适用于GUI组件、模块化系统等场景,提升代码可读性与可维护性。
中介者模式简单代码

1. 中介者模式基本概念与应用场景

中介者模式是一种行为型设计模式,旨在通过引入一个中介对象来封装一系列对象之间的交互关系,从而降低它们之间的直接耦合度。在传统的多对象交互系统中,各个组件往往需要显式地引用彼此以完成通信,导致系统结构复杂、维护困难且扩展性差。中介者模式通过将这些交互逻辑集中到一个“中介者”角色中,使得对象不再相互依赖,而是统一与中介者进行通信,形成一种星形结构。

// 简化示例:同事类仅与中介者通信
public abstract class Colleague
{
    protected IMediator Mediator;
    public Colleague(IMediator mediator) => Mediator = mediator;
}

这种设计不仅提升了系统的模块化程度,也增强了可测试性和可复用性。典型的应用场景包括图形用户界面(GUI)组件间的协调、聊天室系统的消息转发机制、游戏开发中的事件调度系统等。本章为后续理论深化和代码实践奠定基础。

2. 中介者接口(IMediator)设计与定义

在构建可维护、可扩展的软件系统过程中,对象之间的交互复杂度往往会随着功能模块的增长呈指数级上升。尤其是在多组件协同工作的场景中,若缺乏统一协调机制,极易形成“网状依赖”结构,导致代码难以测试、重构成本高、新增功能风险大。为解决这一问题,中介者模式提供了一种将通信逻辑集中管理的设计思路,而其核心起点正是 IMediator 接口 的合理设计。该接口作为整个模式的契约规范,不仅定义了消息传递的基本行为,更决定了系统的解耦程度和未来演进能力。

2.1 中介者模式的角色构成

中介者模式由两个关键角色组成: 中介者(Mediator) 同事类(Colleague) 。它们共同构成了一个星形通信架构,在该结构中,所有交互都必须通过中介者进行转发,而非直接调用彼此的方法或属性。

2.1.1 中介者(Mediator)与同事类(Colleague)的职责划分

在传统面向对象设计中,多个 UI 控件之间可能需要相互通信——例如,当用户在文本框输入内容后,下拉菜单需动态更新选项,同时按钮状态也随之变化。如果采用直接引用方式实现这些联动逻辑,则每个控件都需要持有对其他相关控件的引用,造成高度耦合。

引入中介者模式后,这种关系被重新组织:

  • 中介者(Mediator) 负责接收来自任意同事类的消息,并根据消息类型将其转发给一个或多个目标同事类。
  • 同事类(Colleague) 不再关心谁是消息接收方,只需将消息发送给中介者即可,自身只关注业务逻辑处理。

这种职责分离带来了显著优势。首先,同事类不再依赖具体其他同事类的存在,仅依赖抽象的 IMediator 接口;其次,新增同事类时无需修改已有类的代码,只需向中介者注册对应消息处理器即可完成集成,符合开闭原则。

下表展示了两种设计方式在依赖关系上的对比:

设计方式 通信结构 耦合度 扩展性 维护难度
直接通信 网状结构(N×N)
中介者模式 星形结构(N→1→N)

此外,从职责角度看, Mediator 实例通常承担以下任务:
- 管理同事类的注册与注销;
- 维护消息类型到处理方法的映射表;
- 实现消息广播或多播机制;
- 提供线程安全保证(尤其在并发环境下);
- 支持异步消息处理以提升响应性能。

相比之下, Colleague 类则专注于自身的数据展示、用户输入捕获及本地逻辑执行,完全不需要了解系统中存在哪些其他组件。

2.1.2 星形通信结构中的控制中心定位

星形通信结构是中介者模式最典型的拓扑形态,其中所有同事类都连接至同一个中介者节点,形成类似“Hub-Spoke”的网络模型。

graph TD
    A[TestForm1] --> M[IMediator]
    B[TestForm2] --> M
    C[TestButton] --> M
    D[TestLabel] --> M
    M --> A
    M --> B
    M --> C
    M --> D

如上图所示,所有的消息流动均经过 IMediator 这一中心枢纽。例如,当 TestForm1 触发“数据更新”事件时,它不会直接调用 TestLabel.UpdateText() 方法,而是通过 mediator.Send("DataUpdated", data) 发送消息。中介者收到后,查找所有订阅了 "DataUpdated" 消息类型的同事类,并逐个通知它们。

这种集中式控制的优势体现在以下几个方面:

  1. 降低耦合性 :同事类之间无直接依赖,替换或移除某个同事类不会影响其余组件。
  2. 增强可测试性 :可通过模拟(Mock) IMediator 接口来独立测试各个同事类的行为。
  3. 便于日志记录与监控 :可在中介者层统一添加日志输出、性能追踪等功能。
  4. 支持动态配置 :运行时可根据条件启用/禁用某些消息路由规则。

值得注意的是,虽然星形结构简化了通信路径,但也带来了潜在瓶颈——中介者本身可能成为系统单点故障源。因此,在大型系统中,往往需要将中介者进一步拆分为多个子中介者,按领域划分职责,避免单一中介者过于臃肿。

2.2 IMediator 接口的设计原则

设计一个健壮且灵活的 IMediator 接口,必须遵循现代软件工程的核心原则,尤其是 面向接口编程 依赖倒置 。良好的接口设计不仅能提升系统的可维护性,还能为未来的功能扩展预留空间。

2.2.1 面向接口编程的优势分析

面向接口编程(Interface-based Programming)是指程序模块之间通过抽象接口进行协作,而不是依赖具体实现类。在中介者模式中, IMediator 接口正是这一思想的具体体现。

假设我们有如下接口定义:

public interface IMediator
{
    void Register<T>(string messageType, Action<T> handler);
    void Send<T>(string messageType, T message);
    bool Unregister<T>(string messageType, Action<T> handler);
}

所有同事类在发送或接收消息时,仅依赖这个接口,而不关心背后是由 TestMediator 还是另一个实现了相同契约的类来处理。这意味着我们可以轻松替换不同的中介者实现,比如:

  • 开发环境使用简单的内存中介者;
  • 生产环境切换为支持持久化队列的分布式中介者;
  • 单元测试中使用 Mock 对象验证消息是否正确发出。

这种方式极大提升了系统的灵活性和可替换性。更重要的是,由于接口定义了清晰的行为契约,团队成员可以并行开发不同部分,只要遵守接口约定即可无缝集成。

此外,接口还支持多态性。例如,可以定义多个中介者实现类,分别用于处理不同类型的消息流(如 UI 消息 vs 业务事件),并通过工厂模式动态选择合适的中介者。

2.2.2 方法抽象:消息注册、发送与接收的统一契约

IMediator 接口的核心在于封装三种基本操作: 注册消息处理器 发送消息 取消注册 。这三者共同构成了消息通信的完整生命周期。

消息注册(Register)

允许同事类声明:“我对某种类型的消息感兴趣”,并指定回调方法。

void Register<T>(string messageType, Action<T> handler);
  • messageType :字符串标识符,用于区分不同种类的消息(如 "LoginSuccess" "FileSaved" )。
  • T :泛型参数,表示消息体的数据类型。
  • handler :接收到消息时要执行的动作委托。

此方法使得中介者能够建立一张“消息类型 → 处理函数列表”的映射表,后续发送同类消息时即可批量调用。

消息发送(Send)

用于触发某类消息的广播。

void Send<T>(string messageType, T message);
  • 当某个同事类完成某项操作后,调用此方法通知系统。
  • 中介者查找所有注册了该 messageType 的处理器,并依次调用其 Action<T> 回调。
取消注册(Unregister)

防止内存泄漏的重要机制。

bool Unregister<T>(string messageType, Action<T> handler);
  • 在同事类销毁前应主动注销其注册的处理器,否则可能导致已释放对象仍被调用(引发异常)。
  • 返回 bool 表示注销是否成功,便于调试。

这三个方法共同构成了一个松散耦合的消息总线雏形,也为后续实现提供了标准化入口。

2.3 C# 中 IMediator 的语法实现

C# 语言特性为 IMediator 接口的实现提供了强大支持,尤其是泛型、委托和事件机制的结合使用,使得消息通信既类型安全又高效灵活。

2.3.1 接口声明与泛型支持的可能性探讨

考虑如下完整接口定义:

public interface IMediator
{
    /// <summary>
    /// 注册指定消息类型的处理器
    /// </summary>
    /// <typeparam name="T">消息数据类型</typeparam>
    /// <param name="messageType">消息类型标识符</param>
    /// <param name="handler">处理动作</param>
    void Register<T>(string messageType, Action<T> handler);

    /// <summary>
    /// 发送消息给所有注册该类型的处理器
    /// </summary>
    /// <typeparam name="T">消息数据类型</typeparam>
    /// <param name="messageType">消息类型标识符</param>
    /// <param name="message">消息内容</param>
    void Send<T>(string messageType, T message);

    /// <summary>
    /// 注销指定的消息处理器
    /// </summary>
    /// <typeparam name="T">消息数据类型</typeparam>
    /// <param name="messageType">消息类型标识符</param>
    /// <param name="handler">待注销的处理动作</param>
    /// <returns>是否成功注销</returns>
    bool Unregister<T>(string messageType, Action<T> handler);
}

该接口充分利用了 C# 泛型机制,确保消息传递过程中的类型安全。相比使用 object 类型传递消息,泛型避免了装箱/拆箱操作,也减少了类型转换错误的风险。

例如,发送登录成功的用户信息:

var userInfo = new User { Name = "Alice", Role = "Admin" };
mediator.Send("LoginSuccess", userInfo); // 编译器自动推断 T = User

接收端也必须使用相同的类型注册:

mediator.Register<User>("LoginSuccess", user => 
{
    Console.WriteLine($"欢迎 {user.Name}");
});

若类型不匹配,编译器将报错,从而提前发现错误。

2.3.2 事件委托机制在接口中的整合方式

尽管上述接口未显式包含事件关键字 event ,但其底层实现可借助 C# 的多播委托(MulticastDelegate)来完成高效的事件分发。

例如,在具体中介者类中,可用 Dictionary<string, Delegate> 存储各类消息的处理器集合:

private readonly Dictionary<string, Delegate> _handlers = new();

public void Register<T>(string messageType, Action<T> handler)
{
    if (!_handlers.ContainsKey(messageType))
        _handlers[messageType] = null;

    _handlers[messageType] = (Action<T>)_handlers[messageType] + handler;
}

此处利用了委托的 + 运算符实现多播绑定。当调用 Send 时,只需提取对应委托并动态调用:

public void Send<T>(string messageType, T message)
{
    if (_handlers.TryGetValue(messageType, out var handler))
    {
        var action = handler as Action<T>;
        action?.Invoke(message);
    }
}

这种方法的优点是性能较高,且天然支持一对多广播。缺点是手动管理委托链较复杂,建议封装成专用的事件管理器。

2.4 接口与松耦合架构的关系

2.4.1 依赖倒置原则在中介者模式中的体现

依赖倒置原则(DIP)强调高层模块不应依赖低层模块,二者都应依赖抽象。在中介者模式中, Colleague 类属于高层业务组件,而具体的 Mediator 实现属于底层服务,两者通过 IMediator 接口实现解耦。

public class TestMediaForm : IColleague
{
    private readonly IMediator _mediator;

    public TestMediaForm(IMediator mediator) // 依赖注入
    {
        _mediator = mediator;
        _mediator.Register<string>("ShowMessage", OnMessageReceived);
    }

    public void SendMessage(string msg)
    {
        _mediator.Send("ShowMessage", msg);
    }

    private void OnMessageReceived(string msg)
    {
        Console.WriteLine($"收到消息: {msg}");
    }
}

这里 TestMediaForm 并不关心 _mediator 是哪种实现,只要它符合 IMediator 契约即可。这正是 DIP 的典型应用: 模块间依赖于抽象接口,而非具体类

2.4.2 接口隔离对系统扩展性的提升作用

接口隔离原则(ISP)建议不要强迫客户端依赖它们不用的方法。为此,可将 IMediator 拆分为更细粒度的接口,如:

public interface IMessagePublisher
{
    void Send<T>(string messageType, T message);
}

public interface IMessageSubscriber
{
    void Register<T>(string messageType, Action<T> handler);
    bool Unregister<T>(string messageType, Action<T> handler);
}

同事类可根据需要只实现所需的部分。例如,只发送消息的类只需依赖 IMessagePublisher ,而监听类则依赖 IMessageSubscriber 。这样避免了不必要的方法暴露,增强了封装性和安全性。

综上所述, IMediator 接口不仅是中介者模式的技术基础,更是实现高内聚、低耦合系统的关键支柱。其设计质量直接影响整个系统的稳定性与演化能力。

3. 具体中介者类(TestMediator)实现逻辑

在现代软件架构中,对象间的通信复杂性随着系统规模的扩大呈指数级增长。尤其在图形界面、事件驱动系统或分布式组件交互场景下,若缺乏统一协调机制,极易形成“网状依赖”结构,导致代码难以维护、测试困难且扩展成本高昂。为解决这一问题, 中介者模式 通过引入一个中心化的协调者角色——即“具体中介者类”,来集中管理所有同事对象之间的消息流转。本章将围绕 TestMediator 类的设计与实现展开深入剖析,重点探讨其内部消息路由机制、动态注册策略、线程安全控制以及完整运行流程的技术细节。

TestMediator 作为中介者接口 IMediator 的具体实现类,承担着整个通信体系的核心调度职责。它不仅需要支持灵活的消息类型映射,还需确保在高并发环境下仍能稳定工作。通过对该类的全面解析,可以揭示如何利用 C# 高级语言特性(如泛型、委托、字典集合和锁机制)构建一个高效、可扩展且线程安全的中介服务模块。这种设计思想广泛应用于企业级应用框架、桌面客户端程序及微服务通信中间件中。

3.1 TestMediator 类的功能职责

TestMediator 类是中介者模式中的核心执行单元,其主要功能在于解耦多个同事类(Colleague)之间的直接调用关系,并通过集中式的消息转发机制实现间接通信。该类的核心职责包括两个方面:一是维护一个 消息路由表 ,用于记录不同类型消息与对应处理方法之间的映射关系;二是提供一套完整的 对象生命周期管理机制 ,允许同事类在运行时动态注册或注销自身对特定消息类型的监听。

3.1.1 消息路由表的构建与管理

为了实现灵活的消息分发, TestMediator 使用了一个基于泛型键值对的数据结构来存储消息类型与其处理器之间的关联。这个数据结构通常采用 Dictionary<Type, Delegate> 或更具体的 Dictionary<string, Action<object>> 形式,使得每种消息都可以被唯一标识并绑定到一个或多个回调函数上。

private readonly Dictionary<string, Action<object>> _messageHandlers = 
    new Dictionary<string, Action<object>>();

上述代码定义了一个私有的只读字典 _messageHandlers ,其中键为字符串形式的消息名称(如 "UserLogin" ),值为接受一个 object 类型参数的动作委托。这种设计允许任意类型的对象作为消息负载进行传递,提升了系统的通用性。

该路由表的构建过程发生在同事类调用 Register 方法时。例如:

public void Register(string messageType, Action<object> handler)
{
    if (!_messageHandlers.ContainsKey(messageType))
    {
        _messageHandlers[messageType] = handler;
    }
    else
    {
        _messageHandlers[messageType] += handler;
    }
}

逻辑分析如下:
- 第 2 行检查当前消息类型是否已存在处理器。
- 若不存在,则新建一条映射,将传入的 handler 直接赋值给该键;
- 若已存在,则使用 += 操作符将新处理器添加到多播委托链中,从而支持多个对象监听同一消息。

这种方式实现了 一对多广播机制 的基础支撑。当某类消息触发时,所有注册了该消息的同事类都将收到通知,而无需彼此知晓对方的存在。

此外,路由表还应具备清除无效引用的能力,防止内存泄漏。可通过弱引用(WeakReference)或定期清理机制优化长期运行系统中的资源占用。

属性/方法 类型 说明
_messageHandlers Dictionary > 存储消息名与处理委托的映射
Register() 方法 注册新的消息处理器
Unregister() 方法 移除指定的消息处理器
Send() 方法 根据消息名触发所有匹配的处理器

该表格展示了 TestMediator 中关键成员的基本信息及其作用范围,体现了其作为消息中枢的核心地位。

3.1.2 动态注册与注销同事对象的策略

在实际应用场景中,同事对象可能在程序运行过程中动态创建或销毁(如窗口打开关闭、服务启动停止)。因此, TestMediator 必须支持运行时的注册与注销操作,以保证消息系统的完整性与准确性。

注册机制已在前文描述,下面给出注销实现示例:

public void Unregister(string messageType, Action<object> handler)
{
    if (_messageHandlers.ContainsKey(messageType))
    {
        _messageHandlers[messageType] -= handler;
        // 清理空委托
        if (_messageHandlers[messageType] == null)
        {
            _messageHandlers.Remove(messageType);
        }
    }
}

逐行解释:
- 第 2 行判断指定消息类型是否存在;
- 第 4 行从多播委托中移除指定处理器;
- 第 7–9 行检查移除后该键对应的委托是否为空(即无任何监听者),若是则将其从字典中彻底删除,避免残留无效条目。

此策略确保了系统资源的有效回收,同时也维持了路由表的简洁性。

为进一步提升灵活性,可引入事件机制或观察者模式辅助管理注册状态变化:

graph TD
    A[同事类调用Register] --> B{TestMediator检查消息类型}
    B -->|不存在| C[新增键值对]
    B -->|存在| D[追加至多播委托]
    E[同事类调用Unregister] --> F{查找对应处理器}
    F --> G[从委托链中移除]
    G --> H{是否为空?}
    H -->|是| I[删除字典项]
    H -->|否| J[保留剩余处理器]

该流程图清晰地描绘了注册与注销全过程的决策路径,反映了 TestMediator 在动态环境下的自适应能力。

综上所述, TestMediator 不仅是一个简单的转发器,更是整个通信生态的管理中心。它通过精细化的路由表管理和动态生命周期控制,为系统提供了高度松耦合、易于扩展的消息交互基础。

3.2 消息注册与转发机制的技术实现

消息的注册与转发是中介者模式运作的灵魂所在。只有当消息能够准确地从发送方经由中介者抵达所有感兴趣的接收方时,该模式的价值才能真正体现。为此, TestMediator 需要结合高效的存储结构与强大的回调机制,构建一个可靠的消息传输管道。

3.2.1 基于字典(Dictionary)的消息类型映射存储

选择 Dictionary<TKey, TValue> 作为底层存储结构的主要原因在于其 O(1) 时间复杂度的查找性能,这对于频繁发生的“根据消息类型查找处理器”操作至关重要。

继续深化前文的字段定义:

private readonly Dictionary<string, MulticastDelegate> _handlerMap = 
    new Dictionary<string, MulticastDelegate>();

此处我们将原 Action<object> 替换为更通用的 MulticastDelegate ,以便支持不同签名的委托类型(尽管在实践中常统一为 Action<object> 以简化设计)。

每次注册时,需判断是否已有同名处理器:

public void Register<T>(string msgName, Action<T> handler) where T : class
{
    var wrapper = new Action<object>(obj => handler(obj as T));
    lock (_syncLock)
    {
        if (_handlerMap.ContainsKey(msgName))
        {
            _handlerMap[msgName] = Delegate.Combine(_handlerMap[msgName], wrapper);
        }
        else
        {
            _handlerMap[msgName] = wrapper;
        }
    }
}

逻辑分析:
- 第 2 行通过泛型约束确保消息类型为引用类型;
- 第 4 行创建一个包装委托,将通用 object 转换为目标类型 T 后再执行用户定义的处理逻辑;
- 第 6 行使用 lock 确保多线程环境下的写操作安全;
- 第 8–11 行利用 Delegate.Combine 实现多播委托合并,形成处理器链。

此方式兼顾了类型安全与运行效率,同时保留了未来扩展的可能性。

以下表格对比了不同数据结构在消息路由中的适用性:

数据结构 查找效率 插入效率 是否支持重复键 适用场景
Dictionary O(1) O(1) 高频查询、唯一键
List O(n) O(1) 小规模、允许重复
ConcurrentDictionary O(1) O(1) 多线程高并发
Hashtable O(1) O(1) .NET Framework 兼容

可见,在大多数情况下, Dictionary 或其线程安全版本 ConcurrentDictionary 是最优选择。

3.2.2 多播委托(MulticastDelegate)在消息广播中的应用

C# 中的委托本质上是函数指针的封装,而多播委托则允许一个委托实例持有多个方法引用,依次调用它们。这正是实现“一发多收”消息广播的理想工具。

考虑如下发送方法实现:

public void Send(string msgName, object payload)
{
    Action<object> handlers;
    lock (_syncLock)
    {
        if (!_handlerMap.TryGetValue(msgName, out var delegateObj) || delegateObj == null)
            return;

        handlers = (Action<object>)delegateObj;
    }

    handlers?.Invoke(payload);
}

逐行解读:
- 第 2 行声明局部变量 handlers ,用于脱离锁上下文执行调用,减少锁定时间;
- 第 3–8 行在同步块内获取对应消息的处理器链;
- 第 10 行在外层调用 Invoke ,触发所有注册的方法。

值得注意的是,此处将读取操作也纳入 lock 保护范围,是为了防止在枚举委托链期间发生修改而导致异常(如 InvalidOperationException )。

多播委托的优势在于语法简洁、性能优异,但也存在局限:无法单独控制每个处理器的执行顺序或异常传播。一旦某个处理器抛出异常,默认会中断后续调用。为此可增强发送逻辑:

foreach (var invocation in handlers.GetInvocationList())
{
    try
    {
        invocation.DynamicInvoke(payload);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Handler failed: {ex.Message}");
    }
}

这样即使个别处理器失败,也不会影响整体消息分发流程。

sequenceDiagram
    participant ColleagueA
    participant TestMediator
    participant ColleagueB
    participant ColleagueC

    ColleagueA->>TestMediator: Send("OrderPlaced", orderData)
    TestMediator->>TestMediator: 查找"OrderPlaced"处理器链
    TestMediator->>ColleagueB: 执行B的Receive(orderData)
    TestMediator->>ColleagueC: 执行C的Receive(orderData)
    Note over ColleagueB,ColleagueC: 并行处理(按注册顺序)

该序列图形象展示了消息从发出到广播至多个接收者的全过程,凸显了中介者在协调多方交互中的桥梁作用。

3.3 线程安全与并发访问控制

在多线程环境中,多个同事类可能同时尝试注册、注销或发送消息,若不加以控制,极易引发竞态条件、数据错乱甚至程序崩溃。因此, TestMediator 必须具备健全的并发控制机制。

3.3.1 锁机制(lock)在注册过程中的使用

C# 提供的 lock 关键字是最常用的互斥同步手段。其背后依赖于 Monitor 类,确保同一时刻只有一个线程能进入临界区。

定义一个专用的锁对象:

private readonly object _syncLock = new object();

所有涉及共享状态变更的操作均需加锁:

public void Register(string msgName, Action<object> handler)
{
    lock (_syncLock)
    {
        if (_messageHandlers.ContainsKey(msgName))
        {
            _messageHandlers[msgName] += handler;
        }
        else
        {
            _messageHandlers[msgName] = handler;
        }
    }
}

参数说明:
- _syncLock :避免使用 this typeof(TestMediator) 作为锁对象,以防外部锁定造成死锁;
- lock 块内仅执行必要的共享状态修改,尽量缩短持有锁的时间;
- 对读操作(如 Send 方法中的查找)也建议加锁,以防迭代过程中集合被修改。

虽然 lock 简单有效,但在极高并发场景下可能导致性能瓶颈。此时可考虑改用 ReaderWriterLockSlim ConcurrentDictionary 来提升吞吐量。

3.3.2 异步消息处理的支持路径探索

随着响应式编程和异步模型的普及,越来越多的应用要求非阻塞式消息处理。为此,可在 TestMediator 中引入 Task async/await 支持:

public async Task SendAsync(string msgName, object payload)
{
    List<Func<object, Task>> asyncHandlers;
    lock (_syncLock)
    {
        if (!_asyncHandlerMap.TryGetValue(msgName, out asyncHandlers))
            return;
    }

    foreach (var handler in asyncHandlers)
    {
        await handler(payload); // 或使用 Task.WhenAll 并行执行
    }
}

该方法允许注册异步处理器:

mediator.Register("DataUpdated", async (data) => 
{
    await File.WriteAllTextAsync("log.txt", data.ToString());
});

如此便实现了真正的异步解耦通信,适用于 I/O 密集型任务(如日志记录、网络请求等)。

flowchart LR
    A[线程1: 发送消息] --> B{TestMediator加锁}
    B --> C[更新_handlerMap]
    B --> D[释放锁]
    E[线程2: 同时发送] --> B
    D --> F[异步调用各处理器]
    F --> G[非阻塞返回]

该流程图展现了异步处理模型下的并发协作机制,强调了非阻塞与锁分离的设计优势。

3.4 实际运行流程解析

理解 TestMediator 的理论机制之后,还需通过真实执行流程验证其正确性与健壮性。

3.4.1 消息从发出到被处理的全生命周期追踪

假设系统中有两个同事类: LoginForm DashboardForm ,均通过 TestMediator 进行通信。

  1. 初始化中介者实例;
  2. DashboardForm 注册监听 "UserLoggedIn" 消息;
  3. LoginForm 登录成功后调用 mediator.Send("UserLoggedIn", user)
  4. TestMediator 查找 "UserLoggedIn" 对应的所有处理器;
  5. 逐一调用注册的 Action<object> ,其中包括 DashboardForm 的 UI 更新逻辑;
  6. 完成消息广播,流程结束。

整个过程完全隐藏了 LoginForm DashboardForm 之间的依赖,二者仅依赖于中介者。

3.4.2 调试日志输出辅助理解中介逻辑执行顺序

为便于调试,可在关键节点插入日志:

public void Send(string msgName, object payload)
{
    Console.WriteLine($"[Mediator] Sending message: {msgName}");
    lock (_syncLock)
    {
        if (!_messageHandlers.TryGetValue(msgName, out var handlers))
        {
            Console.WriteLine($"[Mediator] No handlers for {msgName}");
            return;
        }
        Console.WriteLine($"[Mediator] Found {handlers.GetInvocationList().Length} handlers");
    }

    handlers?.Invoke(payload);
    Console.WriteLine($"[Mediator] Message {msgName} dispatched.");
}

输出示例:

[Mediator] Sending message: UserLoggedIn
[Mediator] Found 2 handlers
[DashboardForm] Updating UI...
[Logger] Logging user activity...
[Mediator] Message UserLoggedIn dispatched.

此类日志极大增强了系统的可观测性,有助于快速定位问题。

综上, TestMediator 通过严谨的设计与精细的实现,成功构建了一个高效、安全、可维护的消息中枢,为复杂系统的对象解耦提供了坚实支撑。

4. 同事类接口(IColleague)定义与职责

在复杂系统中,多个对象之间频繁交互是常态。然而,若这些对象直接引用并调用彼此的方法,将导致高度耦合、难以维护和扩展的“网状结构”依赖关系。为解决这一问题,中介者模式引入了 IColleague 接口 作为所有协作对象的行为契约,强制规定它们必须通过统一的方式与中介者通信,而非相互直接调用。该接口不仅是设计上的抽象规范,更是实现松耦合架构的关键一环。

IColleague 的核心作用在于标准化协作流程:每个参与通信的对象都必须实现该接口,从而确保其具备发送消息和接收消息的基本能力。这种统一性使得系统可以动态管理任意数量的同事类实例,并通过中介者进行集中调度。更重要的是,它打破了传统点对点通信模型中的强依赖链,使各个组件能够在不了解其他具体实现的前提下完成交互任务。

从软件工程角度看, IColleague 接口体现了“面向接口编程”的最佳实践原则。通过将行为抽象化,开发者可以在不修改现有代码的基础上新增功能模块,只需实现接口即可接入整个通信体系。例如,在一个企业级管理系统中,订单模块、库存模块和通知模块原本可能需要互相调用,但现在只需各自作为 IColleague 实现类注册到中介者中,便可实现解耦通信。这种方式极大提升了系统的可测试性、可替换性和可维护性。

此外, IColleague 接口还支持运行时动态绑定机制。由于所有同事类都遵循相同的契约,中介者可以在程序执行过程中根据消息类型查找对应的接收方,而无需提前知道其具体类型。这种灵活性特别适用于插件化架构或微服务集成场景,允许第三方组件以最小侵入方式加入已有系统。比如在一个基于事件驱动的消息总线中,任何实现了 IColleague 的服务都可以订阅感兴趣的主题,而发布者完全不必关心谁在监听。

进一步分析可知, IColleague 不仅是一个技术层面的接口定义,更是一种架构思想的体现——即 将通信责任从个体转移到系统层级 。在没有中介者模式的情况下,每个对象都需要自行处理与其他对象的交互逻辑,导致职责混乱;而通过 IColleague + IMediator 的组合,通信逻辑被剥离出来,形成独立的控制中心。这不仅符合单一职责原则(SRP),也为未来引入消息过滤、日志追踪、权限校验等横切关注点提供了良好的扩展基础。

综上所述, IColleague 接口的设计远不止于方法签名的统一,它是构建高内聚、低耦合系统的基石之一。接下来的内容将深入探讨其内部方法的具体设计、参数语义以及与中介者的绑定机制,揭示如何通过这一接口真正实现对象间的高效、安全、可扩展通信。

4.1 IColleague 接口的设计动机

4.1.1 统一协作对象的行为规范

在分布式或多组件系统中,不同模块之间的协作往往缺乏统一的行为标准,导致开发人员不得不针对每种情况编写特定的通信逻辑。这种做法虽然短期内可行,但长期来看会造成代码重复、接口混乱和维护成本上升。为此, IColleague 接口的核心设计动机之一便是建立一套 通用的行为规范 ,使得所有参与通信的类都能按照相同的方式进行消息传递。

通过定义 Send Receive 方法, IColleague 明确了每个同事类必须具备的基本能力:主动发起消息的能力和被动响应消息的能力。这种双向通信契约保证了系统中任意两个组件都可以在中介者的协调下完成信息交换,而无需了解对方的存在。例如,在一个聊天应用中,用户A发送消息时,只需调用自身的 Send 方法并将内容交给中介者,后者会自动路由给所有在线用户(即实现了 IColleague 的其他实例)。整个过程对发送方透明,也无需硬编码接收方列表。

更重要的是,这种规范化的接口设计有助于团队协作开发。当多个开发人员同时实现不同的同事类时,只要他们都遵循 IColleague 接口,就能确保整体通信机制的一致性。即使后期更换某个模块的实现,只要新类仍实现该接口,就不会影响其他部分的功能。这正是接口隔离原则(ISP)的实际应用:将公共行为提取为独立契约,避免因实现细节差异引发系统崩溃。

以下是一个典型的 IColleague 接口定义示例:

public interface IColleague
{
    void Send(string messageType, object message);
    void Receive(string messageType, object message);
    IMediator Mediator { get; set; }
}

上述代码中, Send 方法用于向中介者提交消息,参数包括消息类型标识符和实际数据内容; Receive 方法则由中介者调用,用于通知该同事类有新消息到达; Mediator 属性用于持有中介者引用,确保同事类能够与其通信。

参数名 类型 说明
messageType string 消息类型的唯一标识,通常用于路由匹配
message object 实际传输的数据内容,支持泛型封装
Mediator IMediator 中介者实例引用,用于委托消息发送

该接口的设计充分考虑了扩展性和类型安全性。使用字符串作为消息类型标识符便于分类管理,如 "OrderCreated" "InventoryUpdated" 等;而 object 类型的消息体则允许传递任意结构化数据,结合序列化机制可支持跨进程通信。此外, Mediator 属性的存在使得每个同事类都能访问中介者,但又不直接依赖具体实现,符合依赖倒置原则。

4.1.2 解除同事类之间直接通信的强制约束

传统多对象交互系统中最常见的问题是 紧耦合 。例如,在一个窗体应用程序中,按钮点击后需要更新文本框、刷新列表框并触发后台服务调用。如果采用直接引用方式,按钮控件就必须持有文本框、列表框和服务对象的实例引用,形成复杂的依赖网络。一旦某个组件被修改或移除,整个链条上的对象都可能受到影响。

IColleague 接口的根本目标就是打破这种直接依赖关系。通过要求所有组件实现统一接口并与中介者通信,系统结构由“网状”转变为“星形”,如下图所示:

graph TD
    A[Button Colleague] --> M(Mediator)
    B[TextBox Colleague] --> M
    C[ListBox Colleague] --> M
    D[Service Colleague] --> M
    M --> A
    M --> B
    M --> C
    M --> D

在这个星形结构中,任何同事类都不再需要知道其他类的存在。当按钮触发事件时,它只调用 Mediator.Send("ButtonClick", data) ,而不关心谁会处理这条消息。中介者负责根据注册表找到所有订阅了 "ButtonClick" 类型的接收者,并逐个调用其 Receive 方法。这种机制彻底解除了组件间的直接耦合,使得每个模块都可以独立开发、测试和部署。

为了验证这一优势,考虑以下场景:假设系统需要新增一个日志记录器来监控所有UI操作。在原始紧耦合架构中,必须修改每一个触发操作的控件代码,添加对日志服务的调用;而在中介者模式下,只需创建一个新的 LoggerColleague 类,实现 IColleague 接口并在构造时向中介者注册 "*"(通配符)或特定消息类型 ,即可自动接收到所有广播消息。整个过程无需改动原有任何代码,完美体现开闭原则(OCP)。

此外,解除直接通信约束还有助于提升系统的可测试性。在单元测试中,可以通过模拟(Mock)中介者来验证某个同事类是否正确发送了预期消息,而无需启动整个系统环境。例如:

[Test]
public void WhenButtonClicked_ShouldSendCorrectMessage()
{
    // Arrange
    var mockMediator = new Mock<IMediator>();
    var button = new ButtonColleague(mockMediator.Object);

    // Act
    button.Click();

    // Assert
    mockMediator.Verify(m => m.Send("ButtonClick", It.IsAny<object>()), Times.Once);
}

此测试用例展示了如何利用接口抽象轻松验证行为逻辑,而这一切的前提正是 IColleague 所提供的规范化通信机制。

4.2 接口中方法的抽象设计

4.2.1 发送消息方法(Send)的参数设计与语义定义

Send 方法是 IColleague 接口中最核心的操作之一,其设计直接影响整个通信系统的灵活性与健壮性。理想情况下,该方法应具备足够的表达力以支持多种消息场景,同时保持简洁易用。常见的签名形式如下:

void Send(string messageType, object message);

其中, messageType 是一个字符串常量,代表消息的类别或主题,如 "UserLogin" "DataSaved" 等。选择字符串而非枚举的主要原因是增强扩展性:无需重新编译代码即可新增消息类型,适合插件式系统。此外,字符串还可支持命名空间前缀,如 "Auth.UserLogin" ,便于分组管理。

message 参数采用 object 类型,意味着它可以承载任意数据结构。实践中推荐使用强类型对象并通过泛型包装,例如:

public class Message<T>
{
    public string Type { get; set; }
    public T Payload { get; set; }
    public DateTime Timestamp { get; } = DateTime.Now;
}

这样既能保留类型信息,又能兼容现有接口。在实际调用中:

var loginEvent = new Message<UserInfo>
{
    Type = "UserLogin",
    Payload = currentUser
};
this.Send(loginEvent.Type, loginEvent);

这种方法既保证了语义清晰,又便于后续反序列化处理。

从执行逻辑上看, Send 方法本身并不直接发送消息,而是将其委托给关联的 Mediator 实例。因此,真正的消息转发发生在中介者内部。以下是典型实现片段:

public class BaseColleague : IColleague
{
    public IMediator Mediator { get; set; }

    public virtual void Send(string messageType, object message)
    {
        Mediator?.Send(this, messageType, message);
    }

    public virtual void Receive(string messageType, object message)
    {
        // 默认空实现,子类可重写
    }
}

代码逻辑逐行解读:

  • 第3行:定义 Mediator 属性,用于存储中介者引用,由外部注入。
  • 第6–9行: Send 方法检查中介者是否为空,非空时调用其 Send 方法,并传入当前实例(发送者)、消息类型和内容。此处传递 this 是为了支持发送者识别功能,例如某些消息可能只转发给非发送者的其他同事。
  • 第11–15行: Receive 提供默认空实现,允许子类按需重写处理逻辑。

该设计体现了“委托优于继承”的原则,将通信职责完全交由中介者处理,同事类仅负责触发和响应。

4.2.2 接收消息方法(Receive)的响应机制约定

Send 相对, Receive 方法是被动调用的入口点,由中介者在消息到达时触发。其设计重点在于 如何安全、有序地处理不同类型的消息 。由于消息来源多样且不可预测,必须建立明确的处理规则。

首先, Receive 方法的参数与 Send 保持一致,便于中介者直接转发:

void Receive(string messageType, object message);

接收方需根据 messageType 判断是否处理该消息。常见做法是在子类中使用条件分支或策略模式:

public class TextBoxColleague : BaseColleague
{
    private TextBox _textBox;

    public override void Receive(string messageType, object message)
    {
        if (messageType == "UpdateText")
        {
            if (message is Message<string> textMsg)
            {
                _textBox.Text = textMsg.Payload;
            }
        }
        else if (messageType == "ClearText")
        {
            _textBox.Text = "";
        }
    }
}

参数说明:

  • messageType : 决定处理逻辑的路由键,可用于 switch-case 或字典查找优化。
  • message : 实际数据,需进行类型判断(as/is)以防异常。

为提高性能,可引入消息处理器映射表:

private readonly Dictionary<string, Action<object>> _handlers = new();

protected void RegisterHandler(string messageType, Action<object> handler)
{
    _handlers[messageType] = handler;
}

public override void Receive(string messageType, object message)
{
    if (_handlers.TryGetValue(messageType, out var handler))
    {
        handler(message);
    }
}

此模式将条件判断转化为 O(1) 查找,适用于高频消息场景。

4.3 同事类与中介者的绑定机制

4.3.1 构造函数注入中介者实例的方式

依赖注入是实现松耦合的关键手段,其中 构造函数注入 是最推荐的方式,因其能确保对象创建时即获得所需依赖,避免空引用风险。

public class TestMediaForm : IColleague
{
    public IMediator Mediator { get; private set; }

    public TestMediaForm(IMediator mediator)
    {
        Mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
        Mediator.Register(this); // 自动注册到中介者
    }

    public void Send(string msgType, object msg)
    {
        Mediator.Send(this, msgType, msg);
    }

    public void Receive(string msgType, object msg)
    {
        // 处理逻辑
    }
}

优点:
- 依赖显式声明,易于理解和测试;
- 支持编译期检查,防止遗漏;
- 与 DI 容器天然兼容。

4.3.2 属性注入与方法注入的对比分析

注入方式 优点 缺点 适用场景
构造函数注入 强依赖保障、不可变性 参数过多时构造复杂 核心依赖
属性注入 灵活、支持可选依赖 可能未初始化 配置类、日志器
方法注入 动态传参、上下文相关 调用分散 临时操作

综合来看,构造函数注入最适合 IColleague IMediator 的绑定关系,因其属于必需依赖。

4.4 具体实现类 TestMediaForm 的角色定位

4.4.1 表单组件作为典型同事类的代表性分析

TestMediaForm 是一个典型的 UI 组件类,模拟 WinForms 或 WPF 中的窗体行为。它实现了 IColleague ,代表系统中的一个交互节点,负责响应用户输入并向中介者发布事件。

其代表性体现在:
- 高交互频率 :频繁发送/接收消息;
- 状态敏感 :需及时更新界面;
- 多消息订阅 :监听多种事件类型。

4.4.2 UI 更新与业务逻辑分离的设计思路

通过中介者模式, TestMediaForm 仅关注UI渲染,业务逻辑由后端服务同事类处理,实现关注点分离。

5. 中介者模式完整代码结构与运行流程

5.1 系统整体类图结构展示

为了清晰地表达中介者模式中各个组件之间的关系,我们首先通过 Mermaid 格式绘制类图,直观呈现接口与实现类之间的继承、实现及关联关系。

classDiagram
    class IMediator {
        <<interface>>
        +void Register(string messageType, IColleague colleague)
        +void Send(string messageType, object message, IColleague sender)
    }

    class TestMediator {
        -Dictionary<string, List<IColleague>> _colleagues
        +Register(string messageType, IColleague colleague)
        +Send(string messageType, object message, IColleague sender)
    }

    class IColleague {
        <<interface>>
        +void Send(string messageType, object message)
        +void Receive(string messageType, object message)
    }

    class TestMediaForm {
        -IMediator _mediator
        -string _name
        +TestMediaForm(IMediator mediator, string name)
        +Send(string messageType, object message)
        +Receive(string messageType, object message)
    }

    IMediator <|-- TestMediator
    IColleague <|-- TestMediaForm
    TestMediaForm --> IMediator : 使用
    TestMediator ..> IColleague : 持有列表

如上图所示:
- TestMediator 实现了 IMediator 接口,负责维护消息类型到同事对象的映射。
- TestMediaForm 实现了 IColleague 接口,并持有对 IMediator 的引用,确保所有通信都通过中介者完成。
- 多个 TestMediaForm 实例可注册到同一个 TestMediator 实例中,形成星型拓扑结构。

该类图体现了典型的 依赖倒置原则(DIP) 接口隔离原则(ISP) ,各模块仅依赖抽象,而非具体实现,极大提升了系统的可替换性和测试便利性。

5.2 核心交互流程的逐步执行演示

以下通过一个完整的 C# 示例代码,模拟两个表单组件通过中介者进行通信的过程。我们将分四个阶段详细描述其执行流程。

5.2.1 初始阶段:中介者与多个同事类的实例化

在程序启动时,首先创建中介者实例,并初始化多个同事对象:

IMediator mediator = new TestMediator();

IColleague formA = new TestMediaForm(mediator, "FormA");
IColleague formB = new TestMediaForm(mediator, "FormB");
IColleague formC = new TestMediaForm(mediator, "FormC");

此时,系统内存布局如下:

对象实例 类型 持有中介者引用 已注册消息类型
mediator TestMediator 空字典 _colleagues
formA TestMediaForm 尚未注册
formB TestMediaForm 尚未注册
formC TestMediaForm 尚未注册

每个同事类在构造时接收中介者实例,建立通信桥梁。

5.2.2 注册阶段:各同事类向中介者注册消息类型

接下来,各同事类主动向中介者注册其感兴趣的消息类型:

formA.Send("Register", "LoginRequest");      // FormA 能发送 LoginRequest
formB.Send("Register", "LoginResponse");     // FormB 可处理响应
formC.Send("Register", "Notification");      // FormC 接收通知
formC.Send("Register", "LoginResponse");     // 同一对象可监听多种消息

Send 方法在此处被重载用于注册目的(也可设计专用注册方法),实际调用逻辑如下:

public void Send(string messageType, object message) {
    if (messageType == "Register") {
        _mediator.Register((string)message, this);
    } else {
        _mediator.Send(messageType, message, this);
    }
}

注册完成后, TestMediator 内部的 _colleagues 字典状态为:

消息类型 注册的同事列表
LoginRequest [ ] (无人监听)
LoginResponse [formB, formC]
Notification [formC]

注意:发送方无需关心是否有接收者,实现了完全解耦。

5.2.3 通信阶段:某一同事类发送消息触发转发机制

假设 formA 提交登录请求后,需广播结果:

formA.Send("LoginResponse", new { User = "Alice", Success = true });

此调用进入 TestMediator.Send() 方法:

public void Send(string messageType, object message, IColleague sender) {
    lock (_colleagues) {
        if (_colleagues.TryGetValue(messageType, out var listeners)) {
            foreach (var colleague in listeners) {
                if (colleague != sender) {  // 避免回传
                    colleague.Receive(messageType, message);
                }
            }
        }
    }
}

线程安全锁确保并发注册/发送不会破坏字典结构。

5.2.4 响应阶段:其他同事类接收并处理对应消息

formB formC 分别收到 "LoginResponse" 消息:

public void Receive(string messageType, object message) {
    Console.WriteLine($"{_name} 收到消息 [{messageType}]: {message}");
    // 可进一步解析消息内容并更新 UI 或触发业务逻辑
}

输出示例:

FormB 收到消息 [LoginResponse]: { User = Alice, Success = True }
FormC 收到消息 [LoginResponse]: { User = Alice, Success = True }

整个流程无需 formA 显式知道 formB formC 的存在,也无需事件订阅语法,仅通过字符串消息类型完成路由。

5.3 C# 中依赖注入在模式中的深度应用

5.3.1 使用构造函数注入实现低耦合协作

所有 IColleague 实现均通过构造函数注入 IMediator ,符合控制反转思想:

public TestMediaForm(IMediator mediator, string name) {
    _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
    _name = name;
}

这种设计使得单元测试可以轻松传入 Mock 中介者:

var mockMediator = new Mock<IMediator>();
var form = new TestMediaForm(mockMediator.Object, "TestForm");
form.Send("Register", "TestMsg");

mockMediator.Verify(m => m.Register("TestMsg", It.IsAny<IColleague>()), Times.Once);

5.3.2 结合 IoC 容器提升系统可配置性

在大型项目中,可结合 Autofac、Microsoft.Extensions.DependencyInjection 等容器统一管理生命周期:

services.AddSingleton<IMediator, TestMediator>();
services.AddTransient<IColleague, TestMediaForm>();

并通过工厂模式动态创建同事对象,实现配置驱动的对象协作体系。

5.4 模块化设计方法论总结

5.4.1 多对象交互系统的分层治理策略

将系统划分为三层:
1. 表现层(同事类) :专注自身行为,不参与通信决策;
2. 协调层(中介者) :集中管理消息流转规则;
3. 基础设施层(依赖注入容器) :支撑对象构建与绑定。

这种分层使变更局部化。例如更换消息序列化方式,只需修改中介者内部逻辑,不影响同事类。

5.4.2 中介者模式在大型项目中的适用边界与局限性分析

适用场景 不适用场景
GUI 组件联动(如按钮启用/禁用依赖输入框状态) 高频实时通信(如游戏帧同步)
模块间松耦合通信(微服务间事件总线雏形) 性能敏感型系统(额外转发开销)
需要动态插拔功能模块的插件架构 极简脚本或工具类程序

潜在问题包括:
- 消息命名冲突(建议采用命名空间前缀,如 "Auth.LoginSuccess"
- 内存泄漏(未及时注销导致对象无法释放)
- 调试困难(消息流不易追踪)

可通过引入消息ID、日志追踪链路、可视化监控面板等方式缓解。

最终系统具备良好的横向扩展能力,新增同事类无需修改现有代码,只需注册新消息类型即可融入生态。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:中介者模式是一种行为型设计模式,用于降低多个对象之间的耦合度,将复杂的网状交互关系转化为以中介者为中心的星形结构。通过引入中介者对象协调同事类之间的通信,系统更易于扩展和维护。本文基于C#语言,通过TestMediator和TestMediaForm类展示了中介者模式的具体实现,涵盖接口定义、消息传递机制及类间解耦方法,适用于GUI组件、模块化系统等场景,提升代码可读性与可维护性。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐