支持[EventPriority]标注且强制校验IsHooked的模板模式替代方案
解决方案
核心矛盾
当前模板模式的冲突点非常明确:
- 外部API只会识别直接挂载的回调方法上的
[EventPriority]特性,要求每个事件的挂载入口是独立的、可独立标注特性的方法 - 现有模板方法把公共入口放在基类,所有派生类复用同一个入口方法,无法给每个入口单独加元数据;同时要避免每个派生类重复编写
IsHooked校验逻辑,防止漏写出bug。
方案1:虚方法重写+静态校验(最小改动,推荐中小项目使用)
只需要对原有代码做3处极小调整,完全保留现有模板方法的校验逻辑,没有冗余重复代码,同时支持每个派生类独立配置优先级:
- 把基类的事件入口方法标记为
virtual,保留原有全部IsHooked校验逻辑,核心业务逻辑依然通过抽象的Impl方法强制派生类实现 - 派生类重写入口方法时,仅在方法上添加自身需要的
[EventPriority]特性,方法实现直接调用基类入口,不需要写任何校验或业务逻辑 - 加一段简单的启动/单元测试校验,扫描所有事件实现类,避免重写入口时漏写base调用
代码实现
调整后的DayEndingEvent基类:
internal abstract class DayEndingEvent : Event { // 入口改为虚方法,原有校验逻辑完全保留 internal virtual void OnDayEnding(object? sender, DayEndingEventArgs e) { if (IsHooked) OnDayEndingImpl(sender, e); } protected abstract void OnDayEndingImpl(object? sender, DayEndingEventArgs e); }
具体事件实现类示例:
internal class PlayerSleepDayEndEvent : DayEndingEvent { // 仅在这里配置当前事件的优先级,不需要写任何校验逻辑 [EventPriority(Priority = 100)] internal override void OnDayEnding(object? sender, DayEndingEventArgs e) { // 直接调用基类入口,所有校验逻辑自动复用 base.OnDayEnding(sender, e); } protected override void OnDayEndingImpl(object? sender, DayEndingEventArgs e) { // 这里只写真正的业务逻辑,不需要关心IsHooked状态 Console.WriteLine("执行玩家睡觉当日结算逻辑"); } }
说明:C#反射获取重写方法的元数据时,会读取派生类重写方法上标注的所有特性。EventManager把派生类实例的
OnDayEnding方法挂载到外部API时,外部API可以正常识别到每个类独立配置的[EventPriority],完全满足外部API的限制要求。
配套的校验逻辑(放到项目启动检查或者单元测试里即可,仅需执行一次):
var allEventTypes = typeof(Event).Assembly.GetTypes() .Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(DayEndingEvent))); foreach (var eventType in allEventTypes) { var entryMethod = eventType.GetMethod(nameof(DayEndingEvent.OnDayEnding), BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (entryMethod == null || !entryMethod.IsVirtual || entryMethod.DeclaringType != eventType) { throw new InvalidOperationException($"事件类型{eventType.Name}未正确重写OnDayEnding入口方法,请按规范添加优先级特性并调用base入口"); } }
这个方案整体改动不超过10行原有代码,完全兼容现有所有事件实现,新增事件时的重复代码只有1行base调用,成本极低。
方案2:源生成器方案(零重复代码,推荐事件数量多的项目使用)
如果不想保留那1行base调用,可以写一个简单的C#源生成器,在编译期自动生成所有入口代码,彻底消除重复代码:
- 编译阶段自动扫描所有继承自事件基类的非抽象实现类
- 读取类上标注的
[EventPriority]配置的优先级参数 - 自动为分部类生成挂载到外部API的入口方法,在生成的方法上自动打对应参数的
[EventPriority]特性 - 生成的方法内部直接调用基类的统一执行入口,100%复用
IsHooked校验逻辑,不存在人为漏写的可能
使用时只需要在具体事件类上标注优先级即可,不需要写任何入口相关代码:
// 仅需在类上标注优先级,入口代码全由生成器自动生成 [EventPriority(Priority = 200)] internal partial class CropGrowDayEndEvent : DayEndingEvent { protected override void OnDayEndingImpl(object? sender, DayEndingEventArgs e) { // 仅编写业务逻辑 Console.WriteLine("执行作物生长当日结算逻辑"); } }
避坑提醒
不要为了省基类的改动,直接把入口方法改成抽象方法让每个实现类自己写IsHooked校验。随着项目里事件类型增多,早晚会出现漏写校验导致事件在未激活状态下执行的bug,排查和修复成本远高于上述两个方案的实现成本。
内容的提问来源于stack exchange,提问作者Matheus Leão
相关产品推荐
相关产品推荐

