观察者模式下观察者注册责任归属与可选实现方案咨询
一、通用注册原则
核心是降低依赖耦合,尽量让核心模块不依赖次要模块。你的顾虑完全合理——TimeManager作为核心模块,没必要为了注册观察者就去依赖Portal这种外围次要类,方案A确实会破坏依赖倒置原则,导致耦合度升高。
二、观察者自行注册是否属于良好实践?
结论:不推荐。这种做法会让观察者(比如TimeManager、WaveManager)主动依赖被观察者(Portal),把核心模块和次要模块绑定死。后续如果Portal的逻辑改动、或者需要替换成其他类,核心模块也得跟着修改,违反了开闭原则。而且如果存在多个Portal实例,注册逻辑还得重复处理,维护成本很高。
三、更合理的替代方案
1. 利用现有全局初始化类统一注册
如果项目里已有GameManager、SceneLoader这类全局管理类,完全可以让它们承担注册职责。在场景加载完成后,全局管理器获取三个类的实例,完成事件绑定:
// GameManager 中的初始化逻辑 void Start() { var portal = FindObjectOfType<Portal>(); var timeManager = FindObjectOfType<TimeManager>(); var waveManager = FindObjectOfType<WaveManager>(); portal.OnPortalConditionMet += timeManager.SlowTime; portal.OnPortalConditionMet += waveManager.SpeedUpSpawn; }
这种方式不需要新增专用类,既避免了核心模块依赖次要模块,也不会像方案B那样过度复杂。
2. 事件总线(Event Bus)模式
引入一个轻量的全局事件总线,让三个类彻底解耦:
- Portal不需要维护观察者列表,当条件满足时,直接向总线发布事件;
- TimeManager和WaveManager只需要在初始化时,向总线订阅对应的事件并绑定处理方法。
代码示例:
// 全局事件总线类 public static class EventBus { public static event Action OnPortalConditionMet; public static void TriggerPortalConditionMet() { OnPortalConditionMet?.Invoke(); } } // Portal类的条件检查逻辑 void CheckPortalCondition() { if (/* 条件满足 */) { EventBus.TriggerPortalConditionMet(); } } // TimeManager类的订阅逻辑 void Awake() { EventBus.OnPortalConditionMet += SlowTime; } // WaveManager类的订阅逻辑 void Awake() { EventBus.OnPortalConditionMet += SpeedUpSpawn; }
这种方案下,三个类互相都不知道对方的存在,只依赖事件总线。后续新增响应该事件的模块,只需要订阅总线即可,完全不需要修改Portal的代码,扩展性极强。
3. 依赖注入(DI)框架自动注册
如果项目使用Unity的Zenject、C#的Autofac等DI框架,可以在配置阶段完成事件订阅的绑定。核心模块只需要声明自己要订阅某个事件接口,不需要知道具体的触发源;Portal只需要发布事件,不需要知道订阅者。这种方式解耦彻底,适合中大型项目,但需要一定的DI框架学习成本。
四、方案对比总结
| 方案类型 | 核心优势 | 潜在不足 |
|---|---|---|
| 方案A(观察者自行注册) | 实现简单 | 核心模块依赖次要模块,耦合度高 |
| 方案B(专用中间类注册) | 解耦 | 新增冗余类,过度设计,维护成本高 |
| 全局管理器注册 | 复用现有类,实现简单 | 依赖全局管理器,灵活性稍弱 |
| 事件总线模式 | 完全解耦,扩展性强 | 简单项目可能略显冗余 |
| DI框架自动注册 | 解耦彻底,可维护性高 | 需要学习DI框架,有额外成本 |
结合你的场景(Portal是次要外围类,TimeManager为核心),事件总线模式或者全局管理器注册是最适合的选择,既规避了核心模块的不必要依赖,又不会过度增加复杂度。
内容的提问来源于stack exchange,提问作者vandermies

