如何避免服务定位器反模式

本文关键字:模式 定位器 服务 何避免 | 更新日期: 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容器的耦合,也没有服务位置。这个实现更易于测试,因为类可以用它的实际依赖项来构造,而不需要连接一个容器/解析器。