如何避免服务定位器反模式
本文关键字:模式 定位器 服务 何避免 | 更新日期: 2023-09-27 18:03:41
我试图从抽象基类中删除服务定位器,但我不确定用什么来代替它。下面是我得到的一个伪示例:
public abstract class MyController : Controller
{
protected IKernel kernel;
public MyController(IKernel kernel) { this.kernel = kernel); }
protected void DoActions(Type[] types)
{
MySpecialResolver resolver = new MySpecialResolver(kernel);
foreach(var type in types)
{
IMyServiceInterface instance = resolver.Get(type);
instance.DoAction();
}
}
}
这样做的问题是,派生类的实例化器不知道内核必须有什么绑定才能使MySpecialResolver
不抛出异常。
这可能本质上是难以处理的,因为我不知道从这里我需要解析哪些类型。派生类负责创建types
参数,但它们没有在任何地方进行硬编码。(这些类型是基于在派生类的复合层次结构中存在的属性。)
我试图用延迟加载委托来修复这个问题,但到目前为止我还没有想出一个干净的解决方案。
更新这里有两个问题,一个是IoC容器被传递给控制器,充当服务定位器。这很容易删除——您可以使用各种技术在调用堆栈中向上或向下移动位置。
第二个问题比较困难,当需求直到运行时才公开时,如何确保控制器具有必要的服务。从一开始就应该很明显:你做不到!您将始终依赖于服务定位器的状态或集合的内容。在这种特殊情况下,无论如何摆弄都无法解决本文中描述的静态类型依赖关系问题。我认为我最终要做的是将一个Lazy数组传递到控制器构造函数中,并在缺少所需依赖项时抛出异常。
我同意@chrisichris和@Mark Seemann的观点。
从控制器中删除内核。我会稍微改变一下解析器的组成,这样你的控制器就可以消除对IoC容器的依赖,并允许解析器成为唯一一个担心IoC容器的项目。
然后让解析器传递到控制器的构造函数中。这将使你的控制器更易测试。
例如:public interface IMyServiceResolver
{
List<IMyServiceInterface> Resolve(Type[] types);
}
public class NinjectMyServiceResolver : IMyServiceResolver
{
private IKernal container = null;
public NinjectMyServiceResolver(IKernal container)
{
this.container = container;
}
public List<IMyServiceInterface> Resolve(Type[] types)
{
List<IMyServiceInterface> services = new List<IMyServiceInterface>();
foreach(var type in types)
{
IMyServiceInterface instance = container.Get(type);
services.Add(instance);
}
return services;
}
}
public abstract class MyController : Controller
{
private IMyServiceResolver resolver = null;
public MyController(IMyServiceResolver resolver)
{
this.resolver = resolver;
}
protected void DoActions(Type[] types)
{
var services = resolver.Resolve(types);
foreach(var service in services)
{
service.DoAction();
}
}
}
现在您的控制器没有耦合到特定的IoC容器。此外,您的控制器更易于测试,因为您可以模拟解析器,而根本不需要IoC容器进行测试。
或者,如果您在实例化控制器时无法控制,您可以稍微修改它:
public abstract class MyController : Controller
{
private static IMyServiceResolver resolver = null;
public static InitializeResolver(IMyServiceResolver resolver)
{
MyController.resolver = resolver;
}
public MyController()
{
// Now we support a default constructor
// since maybe someone else is instantiating this type
// that we don't control.
}
protected void DoActions(Type[] types)
{
var services = resolver.Resolve(types);
foreach(var service in services)
{
service.DoAction();
}
}
}
然后在应用程序启动时调用它来初始化解析器:
MyController.InitializeResolver(new NinjectMyServiceResolver(kernal));
我们这样做是为了处理在XAML中创建的元素,这些元素需要解析依赖关系,但我们想删除服务定位器之类的请求。
请原谅任何语法错误:)
我正在写一篇关于在视图模型中使用Service Locator调用重构MVVM应用程序的系列博文,您可能会感兴趣。第2部分即将推出:)
http://kellabyte.com/2011/07/24/refactoring-to-improve-maintainability-and-blendability-using-ioc-part-1-view-models/
也许你应该只是做掉内核,类型和MySpecialResolver,让子类调用DoActions与IMyServiceInterface实例,他们需要直接作为参数。并让子类决定它们如何获得这些实例-它们应该知道最好(或者如果它们不知道究竟是谁决定需要哪些IMyServiceInterface实例)
我本来想在发表这个答案之前有更多的信息,但是Kelly把我放在了现场。:)告诉我要把我的代码放在我的嘴边,可以这么说。
就像我在给Kelly的评论中说的,我不同意将解析器/定位器从静态实现移动到注入实现。我同意ChrisChris的观点,即派生类型需要的依赖关系应该在该类中解决,而不是委托给基类。
也就是说,这里是我如何删除服务位置…
创建命令接口
首先,我会为特定的实现创建一个命令接口。在这种情况下,使用DoActions方法发送的类型是从属性生成的,所以我将创建一个IAttributeCommand
。我正在向命令添加一个Matches
方法,以便声明该命令供某些类型使用。
public interface IAttributeCommand
{
bool Matches(Type type);
void Execute();
}
添加命令实现
为了实现接口,我传入了执行命令(由容器解析)所需的特定依赖项。我在Matches方法中添加了一个谓词,并定义了Execute行为。
public class MyTypeAttributeCommand : IAttributeCommand
{
MyDependency dependency;
SomeOtherDependency otherDependency;
public MyTypeAttributeCommand (MyDependency dependency, ISomeOtherDependency otherDependency)
{
this.dependency = dependency;
this.otherDependency = otherDependency
}
public bool Matches(Type type)
{
return type==typeof(MyType)
}
public void Execute()
{
// do action using dependency/dependencies
}
}
用容器注册命令
在StructureMap(使用您最喜欢的容器),我将注册数组如下:
Scan(s=>
{
s.AssembliesFromApplicationBaseDirectory();
s.AddAllTypesOf<IAttributeCommand>();
s.WithDefaultConventions();
}
根据类型选择并执行命令
最后,在基类上,我在构造函数参数中定义了一个IAttributeCommand
数组,由IOC容器注入。当派生类型传入types
数组时,我将根据谓词执行正确的命令。
public abstract class MyController : Controller
{
protected IAttributeCommand[] commands;
public MyController(IAttributeCommand[] commands) { this.commands = commands); }
protected void DoActions(Type[] types)
{
foreach(var type in types)
{
var command = commands.FirstOrDefault(x=>x.Matches(type));
if (command==null) continue;
command.Execute();
}
}
}
如果你的多个命令可以处理一个类型,你可以改变实现:commands.Where(x=>x.Matches(type)).ToList().ForEach(Execute);
效果是一样的,但是在类的构造方式上有细微的不同。类没有与IOC容器的耦合,也没有服务位置。这个实现更易于测试,因为类可以用它的实际依赖项来构造,而不需要连接一个容器/解析器。