LOADING

加载过慢请开启缓存 浏览器默认开启

Unity 常用面向对象设计模式系列(11):责任链模式(Chain of Responsibility)


一、责任链模式概述

意图:为一系列处理对象创建一条链,使请求沿着链传递,直到有对象处理它或链尾。
别名:职责链、过滤链。

1.1 核心角色

  • Handler(处理者接口)
    定义处理请求的方法以及后续链的引用:
interface IHandler {
    /// <summary>尝试处理请求,返回 true 表示已处理,false 则继续传递</summary>
    bool Handle(EventData data);
    void SetNext(IHandler next);
}

ConcreteHandler(具体处理者)
实现 Handle,对自己关心的请求进行处理,否则调用 next.Handle(data)

Client(客户端)
将各个处理者按顺序 SetNext 串成链,然后发起统一请求:

headHandler.Handle(data);

1.2 UML 类图

ChainOfResponsibilityPattern


二、Unity UI 点击拦截责任链

假设有一个复杂 UI 场景,需要按以下顺序处理点击事件:

  1. 遮挡层UIBlockerHandler)——如果在模态对话框/遮罩区,直接拦截,不再下发
  2. 交互层InteractionHandler)——如果点击可交互按钮或道具,执行对应回调
  3. 统计层LoggingHandler)——记录点击埋点,但不阻断事件

2.1 定义事件数据

public class UIEventData {
    public PointerEventData PointerData;
    // 可扩展:UI 组件引用、事件类型等
}

2.2 抽象基类

public abstract class UIEventHandler : IHandler {
    private IHandler _next;

    public void SetNext(IHandler next) {
        _next = next;
    }

    public bool Handle(EventData data) {
        if (CanHandle(data)) {
            Process(data);
            return true;
        } else if (_next != null) {
            return _next.Handle(data);
        }
        return false;
    }

    /// <summary>
    ///子类决定自己是否能处理该事件
    ///</summary>
    protected abstract bool CanHandle(EventData data);
    /// <summary>
    ///实际的处理逻辑
    ///</summary>
    protected abstract void Process(EventData data);
}

2.3 具体处理者

// 1. 遮挡层:如果点击在 ModalMask 区域,拦截并不向下传递
public class UIBlockerHandler : UIEventHandler {
    protected override bool CanHandle(EventData data) {
        return data.PointerData.pointerEnter != null
            && data.PointerData.pointerEnter.CompareTag("ModalMask");
    }
    protected override void Process(EventData data) {
        // 阻止进一步点击响应
        Debug.Log("UIBlocker: click intercepted");
    }
}

// 2. 交互层:只有可交互组件才处理
public class InteractionHandler : UIEventHandler {
    protected override bool CanHandle(EventData data) {
        return data.PointerData.pointerEnter != null
            && data.PointerData.pointerEnter.TryGetComponent<IInteractable>(out _);
    }
    protected override void Process(EventData data) {
        var interactable = data.PointerData.pointerEnter.GetComponent<IInteractable>();
        interactable.OnInteract();
    }
}

// 3. 统计层:所有未被拦截的点击都记录日志
public class LoggingHandler : UIEventHandler {
    protected override bool CanHandle(EventData data) {
        // 统计层“万金油”,永远返回 true
        return true;
    }
    protected override void Process(EventData data) {
        AnalyticsManager.Instance.TrackEvent(
            "UIClick",
            new Dictionary<string, object> {
                { "object", data.PointerData.pointerEnter?.name ?? "none" }
            }
        );
    }
}

2.4 组装链并发起请求

public class UIInputModule : MonoBehaviour, IPointerClickHandler {
    private IHandler _chainHead;

    void Awake() {
        // 构建链:Blocker → Interaction → Logging
        var blocker     = new UIBlockerHandler();
        var interaction = new InteractionHandler();
        var logging     = new LoggingHandler();
        blocker.SetNext(interaction);
        interaction.SetNext(logging);
        _chainHead = blocker;
    }

    public void OnPointerClick(PointerEventData eventData) {
        var data = new UIEventData { PointerData = eventData };
        _chainHead.Handle(data);
    }
}
  • 点击时,只需调用 _chainHead.Handle(...)
  • 拦截后不会继续传递,保证了责任分离

三、网络消息处理责任链

同样思路可用于服务器或客户端消息分发。以简单 Auth → RateLimit → GameLogic 链为例:

// IMessage 为网络消息基类
public interface IMessageHandler {
    bool Handle(Message msg);
    void SetNext(IMessageHandler next);
}

public abstract class MessageHandler : IMessageHandler { /* 与 UIHandler 类似 */ }

public class AuthHandler : MessageHandler {
    protected override bool CanHandle(Message msg) => !msg.IsAuthenticated;
    protected override void Process(Message msg) {
        // 校验 Token
        msg.IsAuthenticated = AuthService.Validate(msg.Token);
    }
}

public class RateLimitHandler : MessageHandler {
    protected override bool CanHandle(Message msg) {
        return RateLimiter.Allow(msg.UserId);
    }
    protected override void Process(Message msg) { /* 允许通过 */ }
}

public class GameLogicHandler : MessageHandler {
    protected override bool CanHandle(Message msg) => true;
    protected override void Process(Message msg) {
        GameService.Process(msg);
    }
}

// 组装链
IMessageHandler head = new AuthHandler();
head.SetNext(new RateLimitHandler());
head.SetNext(new GameLogicHandler());
head.Handle(incomingMessage);

四、高级话题与扩展

  1. 动态插拔

    • 链可以在运行时 SetNext 调整顺序或插入/移除节点;
  2. 并行链

    • 对于“广播式”处理,可在每个节点 Process 后同时通知多个分支;
  3. 异步链

    • Handle 返回 Task<bool>,支持异步校验(如远程鉴权);
  4. 链式参数

    • EventData 可包含状态 Handled 字段,让节点自行决定是否中断链;
    public bool Handle(EventData data) {
      if (data.Handled) return false;
      if (CanHandle(data)) {
        Process(data);
        data.Handled = true;
      }
      return _next?.Handle(data) ?? false;
    }
    

五、性能与调试

  • 链长度:链过长会增加请求延迟,需合理拆分和组合;

  • 循环检测:防止在动态重组链时产生环形引用,造成无限循环;

  • 日志:可在基类 Handle 前后打印节点名称,快速定位处理流程:

Debug.Log($"[Handler] Trying {this.GetType().Name}");

六、注意事项

场景 陷阱 建议
链式循环 动态插拔不慎导致 A→B→A 循环,Handle 永不返回 SetNext 时检测已存在引用或单向图结构
节点吞掉所有请求 过于宽泛的 CanHandle 导致链无法继续 严格定义 CanHandle 准则,或在处理后显式调用 _next
状态共享冲突 多个节点共享可变数据,链中途修改导致后续节点行为不一致 EventData 设计为不可变或保存中间状态快照
测试困难 客户端直接使用具体链构造,难以隔离某个节点单元测试 将链构造逻辑提取到工厂或配置,测试时 Mock 各节点

七、小结

  1. 职责分离:每个处理者只关注单一责任,CanHandle 与 Process 清晰分离;
  2. 链式解耦:客户端只需持有链头 Handle,无需感知后续节点;
  3. 动态灵活:可通过配置或脚本动态重组链,满足经营策略、A/B 测试等需求;
  4. 日志与监控:加入处理节点日志,定期查看链路性能和处理命中率;
  5. 与其他模式结合:可与 观察者 做事件分发,与 策略 做业务处理解耦。