传递IOC容器的替代方案

4

我有一个带有几个依赖项的基类:

public abstract class ViewModel
{
    private readonly ILoggingService loggingService;

    public ViewModel(
        ILoggingService loggingService,
        ...)
    {
        this.loggingService = loggingService;
        ...
    }
}

在我的派生类中,我不想重复所有这个基类构造函数的参数,所以我做了这个:
public abstract class ViewModel
{
    private readonly IUnityContainer container;
    private ILoggingService loggingService;
    ...

    public ViewModel(IUnityContainer container)
    {
        this.container = container;
    }

    public ILoggingService LoggingService
    {
        get
        {
            if (this.loggingService == null)
            {
                this.loggingService = this.container.Resolve<IUnityContainer>();
            }

            return this.loggingService;
        }
    }

    ...
}

现在我的派生类只需要将一件事传递给我的基类构造函数。我还有一个好处,就是只有在需要时才解决我的依赖关系。
然而,我后来得知传递 IOC 容器是一个不好的主意。考虑到许多服务已经被注册为单例模式,最好的替代设计模式是什么?

1
在我的派生类中,我不想重复所有这个基类构造函数中的参数 - 要么使用属性注入,要么自动生成构造函数并完成它。(反正你也不能真正有意义地将子类与其父类解耦。) - millimoose
9个回答

10

如你所述,应避免将容器传递到不同的部分。这会使其变成一个“物品保管袋”,您无法再查看其依赖关系,并且很难清楚地了解其中有哪些内容。

相反,如果您发现构造函数需要传入太多参数,这本身就是一种编码问题(它违反单一职责原则)。在这种情况下,通常会发现您的类试图做太多事情。

检查您的参数列表,看看能否将其分成小组。例如,如果构造函数需要传入IEmailSenderIEventLogILoggingService,那么您可能需要一个INotificationService,它集成了这三个依赖项。

当然,有时候一个构造函数确实需要传入那么多依赖项。在这种情况下,该类可能只是用于收集这些东西并将它们连接起来。如果是这样的话,该类应该避免执行任何实际工作。


7
在构造函数中传递所有依赖项是最清晰的方法。
我认为在派生类中传递参数没有问题。避免输入错误是错误的动机,而且有像Resharper这样的工具可以帮助您生成这些构造函数。
如果您有很多依赖项,这表明该类违反了单一职责原则。
在许多情况下,更好的选择是组合而不是继承。这也有助于将类分成较小的部分,不违反SRP。

不同意您的观点!我认为您需要传递超过5、6个参数(在我看来4个太多了)- 这会导致您的代码不易读。而且,您可能会在未来想要添加更多参数……我认为更好的做法是将这些参数分组到单独的类中。 - Geka P

1

只需通过容器创建派生类,并将它们注入到需要它们的位置。

错误示例

Foo 担心 Bar 的依赖关系,因为它需要实例化 Bar

class Foo {
    SomeDependency x;
    public Foo(SomeDependency x) {
        this.x = x;
    }
    public doSomething() {
        Bar bar = new Bar(this.x);
        bar.doSomething();
    }

}

class Bar {
    SomeDependency x;
    public Bar(SomeDependency x) {
        this.x = x;
    }
    public void doSomething() {
        // ...
    }
}

正确的例子

Foo 不关心 Bar 是如何创建的。 Bar 直接从容器中获取依赖项。

class Foo {
    public Foo(Bar bar) {
        this.bar = bar;
    }
    public doSomething() {
        this.bar.doSomething();
    }

}

class Bar {
    SomeDependency x;
    public Bar(SomeDependency x) {
        this.x = x;
    }
    public void doSomething() {
        // ...
    }
}

1
我添加了一个小例子来说明我的意思。我强烈推荐观看《清晰代码讲座 - 别再寻找东西!》(http://www.youtube.com/watch?v=RlfLCWKxHJ0),它很好地解释了这个概念。 - bikeshedder
1
但在这个例子中,Bar并没有从Foo派生...所以它与问题无关? - Felix
1
@Felix 添加继承并不改变答案,只会让我的回答更难理解。我有印象问问题的人没有遵循IoC和DI的基本思想:使用容器创建和连接对象。 - bikeshedder
1
我理解这个问题是你有一个 Foo(Dep1 dep),然后一个派生类会有 Bar(Dep1 dep, Dep2 dep2) : base(dep)。如果你有很多派生类,你可以想象每次写出 Dep1 dep 都是不必要的。当然,使用依赖注入,你可能本来就不应该有那么多派生类。 - Felix
1
@EzekielVictor,已经过去十年了。是的,你的更改是正确的。虽然我必须承认,我并没有真正回答OP的问题。与其展示两个草率编写的代码示例,我应该解释一下IoC容器的使用方式:对象不应该依赖于容器,而是容器提供所有依赖项创建对象。 - bikeshedder
显示剩余2条评论

1
避免传递容器。这是服务定位。您必须反转控制,以便创建ViewModel的任何内容都会为您提供依赖的日志记录服务。
Mark Seeman在他的书中非常好地使用了装饰。 AOP是一个整洁的替代方案,正如其他人已经强调的那样。
您的代码应该变成:
public ViewModel(ILoggingService logger)
{
    loggingService= logger;
}

public ILoggingService LoggingService
{
    get
    {
        return this.loggingService;
    }
}

1

CommonServiceLocator 可以通过静态调用方式来解析资源。

Unity文档 使用注入属性 展示了其他选择方式,如果构造函数注入不适用的话。

当我有一个共同基类时,我喜欢使用属性Setter注入进行日志记录:

abstract class Widget
{
   [Dependency]
   public ILogger { set; set; } // Set it and forget it!
}

我从未真正使用过方法调用注入。
你不应该觉得自己做错了什么,只因为你不能设计所有依赖项都通过构造函数传递...在理想的世界里也许可以,但实际上有选择是很好的...

1

1
当你有许多层继承时,这可能会很麻烦,但明确告知类具有哪些依赖关系(通过构造函数)是一件好事。另一种选择是为属性(setter)注入注释对象, 但我建议您仅将其用于可选的依赖项,例如一个日志记录器。

0
我最终使用工厂模式创建了下面的接口和类,以减少添加到构造函数中的参数数量:
public interface IInfrastructureFactory
{
    ILoggingService LoggingService { get; }
    // ... Other Common Services Omitted ...
}

public class InfrastructureFactory : IInfrastructureFactory
{
    private readonly ILoggingService loggingService;
    // ... Other Common Services Omitted ...

    public InfrastructureFactory(
        ILoggingService loggingService,
        // ... Other Common Services Omitted ...
        )
    {
        this.loggingService = loggingService;
        // ... Other Common Services Omitted ...
    }

    public ILoggingService LoggingService
    {
        get { return this.loggingService; }
    }

    // ... Other Common Services Omitted ...
}

在我的IOC容器中,我只注册了一次IInfrastructureFactory。在我的视图模型中,我只有一个依赖项,创建一个新的视图模型更快更简单。
public abstract class ViewModel
{
    private readonly IInfrastructureFactory infrastructureFactory;

    public ViewModel(IInfrastructureFactory infrastructureFactory)
    {
        this.infrastructureFactory = infrastructureFactory;
    }

    public ILoggingService LoggingService
    {
        get { return this.infrastructureFactory.LoggingService; }
    }

    // ... Other Common Services Omitted ...
}

-2

你应该坚持你的第一个模式。如果你已经厌倦了添加那些构造函数变量,那么你可能有太多了。考虑将你的类分解成更小的部分。这个模式非常强大,因为它通过懒惰自我调节 :)

如果你有一个全局类型依赖关系,可能希望在任何地方使用(日志记录是一个完美的例子)...只需使用单例模式...(注意我还假设容器也是使用单例模式创建的)。

public static LoggingService
{
    private static ILoggingService _current;

    public static ILoggingService Current
    {
        get 
        {
            if(_current == null) { _current = Container.Current.Resolve<ILoggingService>(); }
            return _current;  
        }
    }
}

然后像这样使用它...

LoggingService.Current.Log(...);

这样你就不必把它注入到每个东西中了。

一般情况下,除非在很多模块中使用,否则应避免使用这种模式...


2
-1 单例被认为是一种反模式。使用静态成员甚至比传递IoC容器更糟糕。 - bikeshedder

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