是否需要依赖注入实现消息处理器的自动注册?
我有一个RegisterMessageHandlers方法,用于按消息名称注册消息处理器。现在想通过反射自动发现并注册所有处理器——给每个消息标注MessageAttribute,通过反射获取消息名称。但问题是,用Activator实例化处理器时,必须手动传入所有构造函数依赖项。
我目前的解决方案是:把所有依赖注册到DI容器,将IServiceProvider传入MainManagerClass的构造函数,再用ActivatorUtilities.CreateInstance反射实例化每个发现的处理器。但我知道DI和服务定位器有区别,服务定位器是反模式,搞不清二者的界限,所以想问:实现这个需求必须用DI吗?
原始代码:
public class MainManagerClass { private Dictionary<string, MessageHandler> _handlers; private void RegisterMessageHandlers() { _messageHandlers["msg1"] = new MessageHandler1(new Service1()); _messageHandlers["msg2"] = new MessageHandler2(new Service1(), new Service2()); } } public class MessageHandler1 : MessageHandler<Message1> { public MessageHandler1(IService1 service){} } public class MessageHandler2 : MessageHandler<Message2> { public MessageHandler2(IService1 service1, IService2 service2){} } public abstract class MessageHandler<T> : MessageHandler { } public abstract class MessageHandler { } [Message("msg1")] public class Message1 { }
更新后的实现代码:
public class MainManagerClass { private Dictionary<string, MessageHandler> _handlers; private readonly IServiceProvider _serviceProvider; public MainManagerClass(IServiceProvider serviceProvider) { _handlers = new Dictionary<string, MessageHandler>(); RegisterMessageHandlers(); } private void RegisterMessageHandlers() { var messageHandlers = Assembly.GetCallingAssembly() .GetTypes() .Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(MessageHandler))).ToList(); foreach (var handler in messageHandlers) { var messageType = handler.BaseType.GenericTypeArguments[0]; var msgAttribute = messageType.CustomAttributes .Where(t => t.AttributeType == typeof(MessageAttribute)) .FirstOrDefault(); if (msgAttribute == null) throw new Exception($"Message name not defined for message type {messageType.Name}"); var msgName = msgAttribute.ConstructorArguments[0].Value.ToString(); _handlers[msgName] = ActivatorUtilities.CreateInstance(_serviceProvider, handler) as MessageHandler; } } } internal class Program { static async Task Main(string[] args) { var host = CreateHostBuilder().Build(); var manager = new MainManagerClass(host.Services); ... } private static IHostBuilder CreateHostBuilder() { return Host.CreateDefaultBuilder() .ConfigureServices((_, services) => { services .AddSingleton<IService1, Service1>() .AddSingleton<IService2, Service2>() .AddSingleton<IService3, Service3>() ; }); } }
你的需求不是必须依赖DI,但DI是当前场景下最合理、可维护的方案,而且你当前的实现并没有陷入服务定位器反模式,属于DI的合理使用场景。
先明确DI和服务定位器的核心区别
- DI(依赖注入):类只声明自己需要什么依赖,由外部容器主动注入,类本身不关心依赖的来源,符合依赖倒置原则,降低耦合。
- 服务定位器:类主动从容器中索取依赖,导致类与容器强耦合,难以测试,且违背依赖倒置,这才是被诟病的反模式。
你的实现为什么不算服务定位器反模式
你只是在应用初始化阶段(注册处理器时)使用IServiceProvider实例化处理器,这属于DI容器的基础设施代码范畴——服务定位器反模式的核心是在业务逻辑中随处调用容器获取依赖,而你并没有在业务处理流程中依赖容器,只是一次性完成注册工作,完全没问题。
不用DI的替代方案(不推荐)
确实存在不用DI的方式,但会带来严重的维护问题:
- 手动实例化依赖:像你最初的代码那样,直接
new所有依赖,但一旦依赖层级变深(比如Service1依赖Service4,Service4又依赖Service5),手动实例化会变成“依赖地狱”,代码扩展性为零。 - 自己实现简易依赖管理:本质上就是造一个简易DI容器,重复造轮子,稳定性、扩展性都远不如成熟的DI框架。
优化你的当前实现
可以进一步优化,让代码更符合DI规范:
将
MainManagerClass注册到DI容器
不要手动new MainManagerClass(host.Services),而是把它也纳入DI管理:// 在ConfigureServices中添加 services.AddSingleton<MainManagerClass>(); // 在Main中从容器获取 var manager = host.Services.GetRequiredService<MainManagerClass>();自动注册所有处理器到DI,直接注入集合
可以用DI框架的批量注册功能,把所有MessageHandler实现注册到容器,然后在MainManagerClass构造函数中直接注入IEnumerable<MessageHandler>,省去手动实例化的步骤:// 在ConfigureServices中批量注册处理器 services.Scan(scan => scan .FromCallingAssembly() .AddClasses(classes => classes.AssignableTo<MessageHandler>()) .AsSelf() .WithSingletonLifetime()); // 修改MainManagerClass public class MainManagerClass { private Dictionary<string, MessageHandler> _handlers; public MainManagerClass(IEnumerable<MessageHandler> handlers) { _handlers = new Dictionary<string, MessageHandler>(); foreach (var handler in handlers) { var messageType = handler.GetType().BaseType.GenericTypeArguments[0]; var msgAttribute = messageType.GetCustomAttribute<MessageAttribute>(); if (msgAttribute == null) throw new Exception($"Message name not defined for message type {messageType.Name}"); var msgName = msgAttribute.ConstructorArguments[0].Value.ToString(); _handlers[msgName] = handler; } } }
总结
- 实现需求不是必须用DI,但不用DI会带来大量维护成本,得不偿失。
- 你当前的实现不属于服务定位器反模式,是DI的合理使用。
- 进一步优化后,代码会更简洁、符合DI规范,扩展性和可维护性更强。
内容的提问来源于stack exchange,提问作者theateist

