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 容器被传递给控制器​​,充当服务定位器。这很容易删除——您可以使用各种技术将位置向上或向下移动调用堆栈。

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

4

3 回答 3

4

I agree with @chrisichris and @Mark Seemann.

Ditch the kernel from the controller. I'd switch your resolver composition a little bit so that your controller can remove the dependency on the IoC container and allow the resolver to be the only item that worries about the IoC container.

Then I would let the resolver get passed into the constructor of the controller. This will allow your controller to be far more testable.

For example:

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

Now your controller isn't coupled to a specific IoC container. Also your controller is much more testable since you can mock the resolvers and not require an IoC container at all for your tests.

Alternatively, if you don't get to control when a controller is instantiated, you can modify it slightly:

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

You would then call this at your application start up to initialize the resolver:

MyController.InitializeResolver(new NinjectMyServiceResolver(kernal));

We did this to handle elements created in XAML who require dependencies resolved but we wanted to remove Service Locator like requests.

Please excuse any syntactical errors :)

I'm writing a blog post series on the topic of refactoring an MVVM application with Service Locator calls in the view models you might find interesting. Part 2 is coming soon :)

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

于 2011-07-27T05:32:53.293 回答
4

也许你应该去掉 Kernel、Types 和 MySpecialResolver 并让子类直接调用 DoActions 并使用它们需要的 IMyServiceInterface 实例作为参数。并让子类决定他们如何获得这些实例——他们应该最清楚(或者如果他们不知道究竟是哪一个决定需要哪些 IMyServiceInterface 实例)

于 2011-07-26T17:36:56.150 回答
0

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

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

于 2011-07-28T14:23:15.443 回答