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

时间:2011-07-26 17:23:36

标签: c# dependency-injection inversion-of-control service-locator

我正在尝试从抽象基类中删除服务定位器,但我不确定要用什么来替换它。这是我所得到的一个假例子:

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();
        }
    }
}

problem with this是派生类的instanciator不知道内核必须具有什么绑定才能防止MySpecialResolver抛出异常。

这可能是内在难以处理的,因为我从这里不知道我必须解决哪些类型。派生类负责创建types参数,但它们在任何地方都不是硬编码的。 (这些类型基于派生类的组合层次结构中深层属性的存在。)

我试图通过延迟加载代理修复此问题,但到目前为止,我还没有提出一个干净的解决方案。

更新

这里确实存在两个问题,一个是IoC容器传递给控制器​​,充当服务定位器。这很容易删除 - 您可以使用各种技术在调用堆栈中向上或向下移动位置。

第二个问题是困难的问题,如果要求在运行时才暴露,如何确保控制器具有必要的服务。它应该从一开始就很明显:你做不到!您将始终依赖于服务定位器的状态或集合的内容。在这种特殊情况下,任何数量的小问题都不会解决this article中描述的具有静态类型依赖关系的问题。我认为我最终要做的是将一个Lazy数组传递给控制器​​构造函数,并在缺少必需的依赖项时抛出异常。

3 个答案:

答案 0 :(得分:4)

也许您应该放弃Kernel,Types和MySpecialResolver,让子类直接使用他们需要的IMyServiceInterface实例来调用DoActions。让子类决定他们如何到达这些实例 - 他们应该最了解(或者如果他们不知道究竟哪个人决定需要哪个IMyServiceInterface实例)

答案 1 :(得分:4)

我同意@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/

答案 2 :(得分:1)

我希望在发布这个答案之前有更多的信息,但凯利把我当场。 :)告诉我把我的代码放在嘴边,可以这么说。

就像我在对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容器的耦合,也没有服务位置。该实现更具可测性,因为可以使用其真正的依赖关系构建类,而无需连接容器/解析器。