【问题标题】:Alternative to passing IOC container around替代传递 IOC 容器
【发布时间】:2013-01-12 21:59:57
【问题描述】:

我有以下具有多个依赖项的基类:

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 容器是个坏主意。考虑到传入的许多服务已在我的 IOC 容器中注册为单例,最好的替代设计模式是什么?

【问题讨论】:

  • “在我的派生类中,我不想重复这个基类构造函数中的所有参数”——要么使用属性注入,要么自动生成构造函数并完成它。 (无论如何,您并不能真正有意义地将子类与其父类分离。)

标签: c# .net design-patterns inversion-of-control ioc-container


【解决方案1】:

正如您所说,您应该避免传递容器。这把它变成了一个“持有的包”,你不再能看到你的依赖是什么,你也不能轻易看到包里有什么。

相反,如果你发现你的构造函数接受了太多参数,这本身就是一种味道。在这种情况下,您经常会发现您的班级试图做的事情太多(这违反了单一职责原则)。

查看您的参数列表,看看您是否可以将参数分组为更小的组。例如,如果您的构造函数采用IEmailSenderIEventLogILoggingService,那么您真正需要的是聚合这三个依赖项的INotificationService

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

【讨论】:

    【解决方案2】:

    在构造函数中传递所有依赖项是最干净的方法。

    我没有看到在派生类中传递参数的问题。避免打字是错误的动机,Resharper 等工具可帮助您生成这些构造函数。

    如果您有很多依赖项,则表明该类违反了单一责任模式。

    在许多情况下,优先考虑组合而不是继承也是一个好主意。这也有助于将类拆分为不违反 SRP 的较小部分。

    【讨论】:

    • 不同意你的看法!认为你必须传递超过 5,6 个参数(我认为 4 太多了) - 你的代码变得不可读。而且您知道,将来可能您想添加更多参数...我认为更好的做法是将这些参数分组到单独的类中。
    【解决方案3】:

    只需通过容器创建派生类并将它们注入您需要的地方。

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

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

    正确示例 - Foo 不关心 Bar 是如何创建的。 Bar 直接从容器中获取依赖。

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

    【讨论】:

    • 我添加了一个小例子来说明我的意思。我强烈推荐观看The Clean Code Talks - Don't Look For Things!,它很好地解释了这个概念。
    • 但是在这个例子中 Bar 不是从 Foo 派生的...所以它与问题无关?
    • @Felix 添加继承不会改变答案,只会让我的答案更难理解。我的印象是提问者没有遵循 IoC 和 DI 的基本概念:使用容器创建和连接您的对象。
    • 我对这个问题的理解是你有Foo(Dep1 dep),然后派生类有Bar(Dep1 dep, Dep2 dep2) : base(dep)。如果您有很多派生类,您可以想象一直写出Dep1 dep 是不必要的。当然,使用 dep-inj 你可能不应该有那么多派生类
    • 如果bar.doSomething(); 需要进一步的依赖AnotherDependency 怎么办?因此bar.doSomething(AnotherDependency methodDependency);。你如何处理这个案子?
    【解决方案4】:

    AOP 可能是一个解决方案,如果您希望在一系列类中保持一致的行为。这是使用 PostSharp 进行日志记录的示例:http://www.sharpcrafters.com/solutions/logging

    不过,对于临时日志记录,这可能并不完全适合您的需求。

    【讨论】:

      【解决方案5】:

      当您有许多级别的继承时,这可能会很麻烦,但明确告诉类具有哪些依赖项(通过构造函数)是一件好事。另一种选择是Annotating Objects for Property (Setter) Injection,但我建议您仅将其用于可选依赖项,即记录器。

      【讨论】:

        【解决方案6】:

        避免传递容器。这是服务地点。您必须反转控制,以便创建 ViewModel 的任何内容都为您提供相关的日志记录服务。

        马克·西曼在他的装饰书中很好地做到了这一点。正如有人已经强调的那样,AOP 是一个整洁的替代方案。

        你的代码应该变成:

        public ViewModel(ILoggingService logger)
        {
            loggingService= logger;
        }
        
        public ILoggingService LoggingService
        {
            get
            {
                return this.loggingService;
            }
        }
        

        【讨论】:

          【解决方案7】:

          CommonServiceLocator 可以为您提供一种通过静态调用解析资源的方法。

          Unity 文档Using Injection Attributes 显示了在构造函数注入不起作用时可以选择的其他方法。

          当我有一个通用的基类时,我喜欢使用属性设置器注入进行日志记录:

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

          我不能说我曾经真正使用过方法调用注入。

          你不应该仅仅因为你不能设计所有东西来让所有依赖项一直通过构造函数进入,你不应该觉得你做错了什么......也许在一个完美的世界里,但在实践中它很好选项...

          【讨论】:

            【解决方案8】:

            我最终使用工厂模式创建了下面的接口和类,以减少添加到构造函数的参数数量:

            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 ...
            }
            

            【讨论】:

              【解决方案9】:

              你应该坚持你的第一个模式。如果您厌倦了添加这些构造函数变量,那么您的变量太多了。考虑把你的班级分成更小的部分。这种模式非常强大,因为它可以通过懒惰进行自我调节:)

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

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

              这样你就不必将它注入到所有东西中。

              您通常应该避免这种模式,除非它在很多模块中使用...

              【讨论】:

              • -1 单例被认为是一种反模式。使用静态成员比传递 IoC 容器更糟糕。
              猜你喜欢
              • 1970-01-01
              • 2010-11-15
              • 1970-01-01
              • 2014-03-16
              • 1970-01-01
              • 2014-04-29
              • 2011-01-02
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多