[英]How to avoid Service Locator Anti-Pattern?
我正在尝试从抽象基类中删除服务定位器,但我不确定要用什么来替换它。 这是我所得到的一个假例子:
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();
}
}
}
这个问题是派生类的instanciator不知道内核必须具有什么绑定才能使MySpecialResolver
不引发异常。
这可能是内在难以处理的,因为我从这里不知道我将要解决哪些类型。 派生类负责创建types
参数,但它们在任何地方都不是硬编码的。 (这些类型基于派生类的组合层次结构中存在的属性。)
我试图通过延迟加载代理来解决这个问题,但到目前为止我还没有想出一个干净的解决方案。
这里确实存在两个问题,一个是IoC容器被传递给控制器,充当服务定位器。 这很容易删除 - 您可以使用各种技术在调用堆栈中向上或向下移动位置。
第二个问题是困难的问题,如果需求在运行时没有暴露,如何确保控制器具有必要的服务。 它应该从一开始就很明显:你做不到! 您将始终依赖于服务定位器的状态或集合的内容。 在这种特殊情况下,任何数量的小问题都不会解决本文中描述的静态类型依赖关系的问题。 我认为我最终要做的是将一个Lazy数组传递给控制器构造函数,并在缺少必需的依赖项时抛出异常。
也许你应该放弃Kernel,Types和MySpecialResolver,让子类直接使用他们需要的IMyServiceInterface实例来调用DoActions。 让子类决定他们如何到达这些实例 - 他们应该最了解(或者如果他们不知道究竟哪个人决定需要哪个IMyServiceInterface实例)
我同意@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部分即将推出:)
在发布这个答案之前,我本来希望得到更多信息,但凯利让我当场。 :)告诉我把我的代码放在嘴边,可以这么说。
就像我在对Kelly的评论中所说的那样,我不同意将解析器/定位器从静态实现移动到注入的实现。 我同意ChrisChris的观点,即派生类型所需的依赖项应该在该类中解析,而不是委托给基类。
也就是说,这是我将如何删除服务位置...
首先,我将为特定实现创建一个命令接口。 在这种情况下,使用DoActions方法发送的类型是从属性生成的,因此我将创建一个IAttributeCommand
。 我正在为命令添加一个Matches
方法,以声明该命令供某些类型使用。
public interface IAttributeCommand
{
bool Matches(Type type);
void Execute();
}
为了实现接口,我传入了执行命令所需的特定依赖项(由我的容器解析)。 我在我的Matches方法中添加了一个谓词,并定义了我的执行行为。
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容器的耦合,也没有服务位置。 该实现更具可测性,因为可以使用其真正的依赖关系构建类,而无需连接容器/解析器。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.