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

14

我正在尝试从一个抽象基类中移除一个服务定位器,但我不确定要用什么来替换它。以下是我所拥有的伪代码示例:

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数组传递到控制器构造函数中,并在缺少必需的依赖项时抛出异常。


4
嗯,我知道这不是时髦的答案,但服务定位器(Service Locator) 不是 反模式。它是使用IoC容器的一种替代方式,但您可以在任何一种方式中使用依赖注入(向服务定位器类注入依赖有点奇怪,但并非不可能)。话虽如此,如果您正在使用IoC容器,那么再使用服务定位器就可能会被视为代码异味。 - Daniel Pryden
看起来你正在使用MVC和可能是Windsor,但我不确定 - 如果是这样,你可以使用http://commonservicelocator.codeplex.com/,然后在Application_Start中调用ServiceLocator.SetLocatorProvider(() => { return new WindsorServiceLocator(_container); });DependencyResolver.SetResolver(ServiceLocator.Current); 然后这将允许您将依赖项注入到控制器构造函数中。其他容器也受支持,但我只用过Windsor。 - Adam
Paul,你能添加一个派生类属性依赖的例子吗?这些属性是在派生控制器上还是在派生控制器的依赖项上? - codeprogression
@Richard,这些基于派生控制器属性--位于另一个程序集中。因此,虽然所需的依赖项在构建时技术上是静态的,但净效应是,在控制器被实例化之前,它们对IoC容器或单元测试是未知的。 - Paul
1
尽管这是一种“反模式”,但我发现传递内核接口非常有用,特别是在测试和能够使用Ninject.Mock并传递内核的Mock版本时。 - Makotosan
显示剩余10条评论
3个回答

4
也许你应该放弃Kernel、Types和MySpecialResolver,并让子类直接使用它们需要的IMyServiceInterface实例作为参数调用DoActions方法。并且让子类决定如何获得这些实例 - 他们应该最清楚(或者如果他们不知道哪个人决定需要哪些IMyServiceInterface实例)。

1
似乎类型数组是关键。去掉它,其余部分应该很简单。 - Mark Seemann
@MarkSeemann,所以基本上要放弃所有的动态性,采用紧密耦合、脆弱的解决方案。太棒了。我们何必使用多态性,当我们可以在一个巨大的方法中编写所有内容时呢? - Chris Marisic

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中创建的需要解决依赖关系的元素,但我们想要消除类似于服务定位器的请求。
请原谅任何语法错误 :)
我正在撰写一篇有关使用服务定位器调用视图模型的MVVM应用程序重构主题的博客文章系列,您可能会感兴趣。第二部分即将推出 :)

http://kellabyte.com/2011/07/24/refactoring-to-improve-maintainability-and-blendability-using-ioc-part-1-view-models/


在我看来,用注入的服务定位器(或解析器)替换静态服务定位器并不能解决手头的问题。这样设计仍然很脆弱,因为我必须知道要向解析器注册哪些依赖项以测试我的类。就像在提出的问题中一样,派生类仍然不知道需要什么“内核”依赖项。 - codeprogression
1
没问题,我会发表我的建议。(不过我希望先有更多的信息。)目前的问题是一个 IOC 容器被传递给了一个类。这个类负责的事情超出了它应该承担的范围,我们正试图通过 DI/IOC 来解决这个问题。 - codeprogression
@codeprogression 因为我必须知道要向解析器注册哪些依赖项来测试我的类,这是错误的。你不需要测试这个。你测试类是否调用了服务定位器,而不是测试服务(假设你正在测试 MyController)。每个单独的服务都应该有自己的独立测试。这是正确的服务定位的最大优点之一。你可以组合和排序极易测试的服务来完成惊人的事情。要测试组合,请进行完整的堆栈测试,否则没有意义。 - Chris Marisic
未能在全栈场景下测试服务组合会导致虚假期望。因此,在集成测试中启动MyController,配置定位器以返回X、Y、Z。现在所有的测试都通过了,你有一种虚假的安全感。开发人员进去更改服务注册代码以返回A、B、C。你的集成测试通过了,但你的网站崩溃了。 - Chris Marisic
我喜欢你的解决方案,但我也不喜欢你的解决方案。当你考虑它时,它只是一个被阉割的服务定位器(解析器),你将其注入构造函数中。 - Ondřej

0

在发布这个答案之前,我希望有更多的信息,但是Kelly让我感到有些被动。 :) 可以说是让我把代码放在嘴边。

就像我在对Kelly的评论中所说的那样,我不同意将解析器/定位器从静态实现移动到注入实现。我同意ChrisChris的观点,即派生类型需要解决其需要的依赖项,并且不应委托给基类。

话虽如此,以下是我将如何删除服务位置...

创建命令接口

首先,我会为特定实现创建一个命令接口。在这种情况下,使用DoActions方法发送的类型是由属性生成的,因此我会创建一个。我正在向命令添加一个方法,以便声明用于某些类型的命令。

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 容器耦合,也没有服务定位。这种实现方式更易于测试,因为该类可以使用其真实依赖项进行构造,而无需连接容器/解析器。


1
但这只是一个原始的服务定位器。在任何IoC容器的内部,都有类似于您在此处编写的内容的东西。IAttributeCommand数组本身不解析类型,但概念相同。 - Paul
这是否意味着您根本没有使用IOC容器?如果是这样,那么您将很难删除服务定位代码。 - codeprogression
1
当你有服务定位DoActions(Type[] types)时,根据定义,存在服务定位。谁来处理这些位是一个相当微不足道的问题。良好应用服务定位会导致极其动态和可扩展的系统。而服务定位应用不当则会导致极其脆弱和不可扩展的系统。这是软件架构的基本基础。解决正确的问题会带来成功,解决错误的问题会导致一堆麻烦... - Chris Marisic

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接