【发布时间】:2019-12-11 14:18:00
【问题描述】:
我们正在将庞大的代码库从 .NET 框架迁移到 .NET 核心。不幸的是,我们正在迁移的一些代码存在设计异味,但在这个阶段我们不能随意破坏,我们需要仔细计划随着时间的推移进行更改。
主要问题之一是我们的代码依赖于大量抽象工厂,这些工厂已知为a code smell(至少在被滥用时)。一个相关的问题是 Castle Windsor 容器在现有代码中被广泛使用,我们希望避免在 ASP.NET 核心中使用它,我们更愿意坚持使用默认内置的 DI 容器。
我试图了解是否可以通过使用 ASP.NET 核心 DI 容器来重写我们目前基于城堡温莎容器的一些抽象工厂实现。根据我对依赖注入的理解,在应用程序的组合根中编写这类类不是问题;换句话说,具有依赖于 DI 容器的组合根类不是一种引入service locator 代码气味的方法。
目前我们抽象工厂的接口定义是一个有漏洞的,因为它公开了一个Release 方法,该方法只是为了遵守Castle Windsor 的register resolve release pattern。这是当前的接口定义:
public interface ICommandHandlerFactory
{
ICommandHandler CreateHandler(Type commandType);
void Release(ICommandHandler handler);
}
具体实现依赖于 Castle Windsor 容器,以便解析命令处理程序并在用户完成时释放它们:
public class WindsorCommandHandlerFactory
{
private readonly IKernel _container;
public WindsorCommandHandlerFactory(IKernel container)
{
_container = container;
}
public ICommandHandler CreateHandler(Type commandType)
{
// here we create the command handler type from the command type and then
// we ask the container to resolve the command handler type
}
public void Release(ICommandHandler handler)
{
_container.ReleaseComponent(handler);
}
}
我的问题与对象生命周期管理有关。关键是对于 ASP.NET 核心 DI 容器,生命周期管理不是基于 Release 方法,而是基于
scope 的概念。
最佳实践基本上是从应用程序根容器创建一个作用域,从作用域的容器解析依赖关系,使用依赖关系并最终处置作用域。当作用域被释放时,作用域内解析的作用域和瞬态服务将被停用并避免内存泄漏。
这是对应的代码:
public class Worker
{
private readonly IServiceProvider container;
public Worker(IServiceProvider container)
{
_container = container;
}
public void DoStuff()
{
using(var scope = container.CreateScope())
{
var service = scope.ServiceProvider.GetRequiredService<IService>();
service.Work();
}
}
}
为了坚持这个设计,我应该清楚地简化抽象工厂定义,通过删除泄漏(现在不再有用)Release 方法:
public interface ICommandHandlerFactory
{
ICommandHandler CreateHandler(Type commandType);
}
鉴于这个接口定义,我该如何应对容器作用域的创建和处置?
我不能简单地做以下事情,因为当作用域被释放时,已解决的依赖关系被解除,因此调用代码可能会引用已释放的对象强>:
// this WON'T work due to the scope disposal when the service is returned
public class CommandHandlerFactory
{
private readonly IServiceProvider container;
public CommandHandlerFactory(IServiceProvider container)
{
_container = container;
}
public ICommandHandler CreateHandler(Type commandType)
{
using(var scope = container.CreateScope())
{
Type commandHandlerType = ... // build the command handler type starting from the command type
var service = scope.ServiceProvider.GetRequiredService(commandHandlerType);
return service;
}
}
}
你有什么想法吗?
更新
再次仔细阅读this article后,我意识到这个设计问题的解决方案非常明显。
讨论的重点是抽象工厂设计模式的内在问题,所以解决的方法可能是完全避免抽象工厂。
下面的代码非常自动解释,并展示了一种用另一个抽象(在示例中称为ICommandDispatcher)替换抽象工厂的方法,该抽象基本上是一个适配器,用于为要处理的命令调用正确的命令处理程序。
public interface ICommand
{
}
public interface ICommandHandler<T> where T : ICommand
{
void Handle(T command);
}
public interface ICommandDispatcher
{
void Dispatch(ICommand command);
}
public class CommandDispatcher : ICommandDispatcher
{
private readonly IServiceProvider _container;
public CommandDispatcher(IServiceProvider container)
{
_container = container ?? throw new ArgumentNullException(nameof(container));
}
public void Dispatch(ICommand command)
{
if (command == null)
throw new ArgumentNullException(nameof(command));
var commandType = command.GetType();
var commandHandlerType = typeof(ICommandHandler<>).MakeGenericType(commandType);
using (var scope = _container.CreateScope())
{
var commandHandler = scope.ServiceProvider.GetRequiredService(commandHandlerType);
((dynamic)commandHandler).Handle((dynamic)command);
}
}
}
现在所有以前依赖于ICommandHandlerFactory 的代码都应该依赖于新的抽象ICommandDispatcher。只需记住在应用程序的组合根中定义类CommandDispatcher(组合根是允许依赖于 IOC 容器的代码的唯一模块)。
【问题讨论】:
-
我认为迁移到 .Net Core 并远离 Castle Windsor(以及任何其他修复)可能应该分步完成。当事情已经足够复杂时,为什么还要复杂化。假设 Castle Windsor 适用于 .Net Standard/.Net Core
-
不要。不要那样做。迁移本身就够复杂了。
标签: c# asp.net-core design-patterns dependency-injection ioc-container