在实际项目中,我们经常遇到这样的场景:有一组策略类,每个策略处理不同的数据类型,但调用方只需要一个统一的非泛型接口来调度它们。问题在于——泛型接口 IFoo<T> 无法直接放进 List<IFoo> 里,因为 IFoo<A> 和 IFoo<B> 是不同的类型。
今天我们从 SwitchData 项目的任务执行器模块出发,看一个优雅的解决方案:泛型接口显式实现(Explicit Interface Implementation)。
问题背景
SwitchData 是一个网元数据采集与解析系统,支持多种任务类型(采集、解析、UPF地址解析等),每种任务携带不同的参数数据:
| 任务类型 | 参数类 | 说明 |
|---|---|---|
| Collect(采集) | CollectTaskPayload | 无额外参数 |
| Parse(解析) | ParseTaskPayload | 携带网元组ID或设备ID |
| ParseUpfAddress | ParseUpfAddressTaskPayload | 无额外参数 |
系统需要一个工厂,根据任务类型返回对应的执行器。但每种执行器处理的参数类型不同,如何统一调度?
双层接口设计
核心思路是设计两个接口——一个非泛型接口供工厂和调度层使用,一个泛型接口供具体执行器使用:
// 非泛型接口:工厂返回类型,调度层使用
public interface INeTaskExecutor
{
NeTaskType TaskType { get; }
Type PayloadType { get; }
Task<JsonDataResult> ExecuteAsync(NeTaskExecution execution, object payload,
PerformContext context, CancellationToken cancellationToken = default);
string CreateIdempotencyKey(int? taskId, object payload);
bool TryGetPayload(string payloadJson, out object payload);
}
// 泛型接口:具体执行器使用,获得类型安全
public interface INeTaskExecutor<TPayload> : INeTaskExecutor
{
Task<JsonDataResult> ExecuteAsync(NeTaskExecution execution, TPayload payload,
PerformContext context, CancellationToken cancellationToken = default);
}
注意关键区别:
- 非泛型接口的
ExecuteAsync参数是object payload,所有执行器共享同一签名 - 泛型接口的
ExecuteAsync参数是TPayload payload,子类获得强类型
两者都叫 ExecuteAsync,参数列表不同,编译器如何区分?
显式接口实现:桥接泛型与非泛型
答案就是 显式接口实现。抽象基类 NeTaskExecutorBase<TPayload> 同时实现两个接口,用显式实现将非泛型方法"隐藏"起来,内部做类型转换后委托给泛型版本:
public abstract class NeTaskExecutorBase<TPayload> : INeTaskExecutor<TPayload>
{
public abstract NeTaskType TaskType { get; }
public Type PayloadType => typeof(TPayload);
// 抽象方法:子类实现,享受强类型 TPayload
public abstract Task<JsonDataResult> ExecuteAsync(NeTaskExecution execution,
TPayload payload, PerformContext context, CancellationToken cancellationToken = default);
public abstract string CreateIdempotencyKey(int? taskId, TPayload payload);
// 显式实现 INeTaskExecutor(非泛型)—— 桥接层
async Task<JsonDataResult> INeTaskExecutor.ExecuteAsync(
NeTaskExecution execution, object payload,
PerformContext context, CancellationToken cancellationToken)
{
return await ExecuteAsync(execution, (TPayload)payload, context, cancellationToken);
}
// 显式实现:JSON 反序列化,统一处理
bool INeTaskExecutor.TryGetPayload(string payloadJson, out object payload)
{
try
{
payload = JsonHelper.Deserialize<TPayload>(payloadJson);
return payload != null;
}
catch
{
payload = default;
return false;
}
}
// 显式实现:幂等键生成,委托给泛型版本
string INeTaskExecutor.CreateIdempotencyKey(int? taskId, object payload)
{
return CreateIdempotencyKey(taskId, (TPayload)payload);
}
}
显式实现的三个关键效果
-
方法不可见:显式实现的方法无法通过类的实例直接调用,只能通过接口引用访问。这避免了两个重载同时暴露造成的混淆。
-
类型转换集中管控:
object -> TPayload的强制转换只在显式实现中发生一次,子类永远不需要手动转型。 -
通用逻辑复用:
TryGetPayload的 JSON 反序列化逻辑在基类统一实现,子类无需关心反序列化细节。
具体执行器:只关心业务
有了基类,具体执行器变得极其简洁,只需关注业务逻辑:
public class CollectTaskExecutor : NeTaskExecutorBase<CollectTaskPayload>
{
public override NeTaskType TaskType => NeTaskType.Collect;
public override string CreateIdempotencyKey(int? taskId, CollectTaskPayload payload)
{
if (!taskId.HasValue)
throw new ArgumentNullException(nameof(taskId));
return $"TaskId={taskId}";
}
public override async Task<JsonDataResult> ExecuteAsync(
NeTaskExecution execution, CollectTaskPayload payload,
PerformContext context, CancellationToken cancellationToken = default)
{
return await NeLogService.CollectLogFilesAsync(cancellationToken);
}
}
public class ParseTaskExecutor : NeTaskExecutorBase<ParseTaskPayload>
{
public override NeTaskType TaskType => NeTaskType.Parse;
public override string CreateIdempotencyKey(int? taskId, ParseTaskPayload payload)
{
ArgumentNullException.ThrowIfNull(payload);
if (taskId.HasValue) return $"TaskId={taskId}";
if (payload.NeGroupId.HasValue) return $"TaskType={(int)TaskType}:NeGroupId={payload.NeGroupId}";
if (payload.DeviceId.HasValue) return $"TaskType={(int)TaskType}:DeviceId={payload.DeviceId}";
throw new InvalidOperationException("网元组ID与设备ID不能同时为空值");
}
public override async Task<JsonDataResult> ExecuteAsync(
NeTaskExecution execution, ParseTaskPayload payload,
PerformContext context, CancellationToken cancellationToken = default)
{
if (payload.NeGroupId.HasValue)
return await NeLogService.ParseAndPersistGroupAsync(
payload.NeGroupId.Value, execution.Id, true, cancellationToken);
else if (payload.DeviceId.HasValue)
return await NeLogService.ParseAndPersistDeviceAsync(
payload.DeviceId.Value, execution.Id, true, cancellationToken);
else
return new JsonDataResult { success = false, message = "网元组ID与设备ID不能同时为空值" };
}
}
子类直接使用 ParseTaskPayload,无需任何转型,编译器全程保障类型安全。
工厂:统一注册与分发
工厂利用非泛型接口 INeTaskExecutor 来统一管理所有执行器:
public static class NeTaskExecutorFactory
{
private static readonly Dictionary<NeTaskType, INeTaskExecutor> _executors;
static NeTaskExecutorFactory()
{
_executors = new Dictionary<NeTaskType, INeTaskExecutor>();
var executors = new INeTaskExecutor[]
{
new CollectTaskExecutor(),
new ParseTaskExecutor(),
new ParseUpfAddressTaskExecutor()
};
foreach (var executor in executors)
{
_executors.Add(executor.TaskType, executor);
}
}
public static INeTaskExecutor GetExecutor(NeTaskType taskType)
{
if (_executors.TryGetValue(taskType, out var executor))
return executor;
throw new NotSupportedException($"不支持任务类型:{taskType}");
}
}
这里的关键在于 INeTaskExecutor[]——因为所有执行器都实现了非泛型接口,所以可以放进同一个数组。而调用方拿到的虽然是 INeTaskExecutor(参数是 object),但实际上转型安全,因为显式实现内部做了 (TPayload)payload。
调度层:完整的执行流程
在调度入口 NeTaskExecutor 中,整个流程串起来:
private static async Task ExecuteAsync(
NeTaskExecution execution, PerformContext context, CancellationToken cancellationToken)
{
execution.StartTime = DateTime.Now;
try
{
// 1. 工厂获取执行器(非泛型接口)
var executor = NeTaskExecutorFactory.GetExecutor(execution.TaskType);
// 2. JSON 反序列化为强类型 payload(显式实现统一处理)
if (!executor.TryGetPayload(execution.PayloadJson, out var payload))
throw new InvalidOperationException("负载JSON不合法");
// 3. 执行(内部自动转型为 TPayload,调用子类的泛型方法)
var result = await executor.ExecuteAsync(execution, payload, context, cancellationToken);
execution.Status = result.success ? ExecutionStatus.Success : ExecutionStatus.Failed;
execution.Message = result.message;
}
catch (Exception ex)
{
execution.Status = ExecutionStatus.Failed;
execution.Message = ex.Message;
}
execution.EndTime = DateTime.Now;
await NeTaskExecutionService.UpdateAsync(execution, cancellationToken);
}
扩展:新增任务类型只需三步
当需要新增一种任务类型时,只需要:
- 定义 Payload 类,实现
ITaskPayload - 创建执行器类,继承
NeTaskExecutorBase<YourPayload> - 在工厂的
executors数组中注册
无需修改调度层、无需修改基类、无需修改任何已有执行器——完美符合开闭原则。
显式实现 vs 隐式实现:何时选择?
| 特性 | 隐式实现 | 显式实现 |
|---|---|---|
| 可通过类实例直接调用 | 是 | 否,必须通过接口引用 |
| 可设为 virtual/abstract | 是 | 否 |
| 适合场景 | 公开API | 桥接层、适配器模式 |
| 多接口同名方法冲突 | 无法区分 | 可以区分 |
在本文的场景中,显式实现是唯一选择——因为 INeTaskExecutor.ExecuteAsync(object) 和 INeTaskExecutor<TPayload>.ExecuteAsync(TPayload) 如果都用隐式实现,编译器会报错(参数类型不同不算合法重载,而是同名方法冲突)。
总结
泛型接口的显式实现是一个被低估的 C# 特性,它在以下场景中发挥关键作用:
- 桥接泛型与非泛型世界:让工厂、调度层等基础设施代码使用
INeTaskExecutor,具体业务代码使用INeTaskExecutor<TPayload> - 集中类型转换:
object -> T的转型只在基类的显式实现中出现,子类永远操作强类型 - 统一通用逻辑:JSON 反序列化、幂等键生成等横切关注点在基类一次性处理
- 符合开闭原则:新增策略只需继承基类并注册,不改动已有代码
下次当你需要一组处理不同数据类型的策略,又需要统一的调度入口时,试试泛型接口 + 显式实现这个组合。