【问题标题】:How to new up objects when using DI使用 DI 时如何新建对象
【发布时间】:2018-10-20 13:58:20
【问题描述】:

我刚刚在 PluralSight 上观看了一个依赖注入课程,它为我解决了一些问题。但尽管它跨越了几层,但也没有关于当你只需要在你的代码中更新对象时究竟要走什么路的信息。

这是我难以掌握的最后一块拼图。我知道 Compisition Root 以及大致如何使用它。我知道要避免使用 ServiceLocator 并通过将 IoC 容器定义为代码库中几乎每个类中的 ctor 参数来避免将 IoC 容器传递到代码的所有级别(我都曾在我工作的公司中并排看到过)为了)。

让我们考虑下面两个例子,我认为把所有东西都交出来是不切实际的。

我可以注入依赖,但我不可能注入所有东西,毕竟它不叫对象注入,所以问题是如何在循环内动态创建Doee对象:

public class MyClass
{
    private IVerifyer _verifyer;
    public MyClass(IVerifyer verifyer)
    {
        _verifyer = verifyer;
    }

    public IList<Doer> PrepareDoingSomething(IList<IDoees> doees)
    {
        var doers = new List<Doer>();
        foreach (var doee in doees)
        {
            if (!Verifyer.Verify(doee)) throw new Exception("Blablabla...");
            doers.Add(new Doer(doee));
        }
        return doers;
    }
}

现在在一些“不太动态”的情况下,问题是对于一些 IO 类,既没有接口也没有抽象基类可用,这意味着它们很难在测试代码中处理,我也不会甚至知道如何使用 IoC 容器来处理它们,例如 Process 类:

public class Processor
{
    public Process ProcessSomething(IProcessee processee)
    {
        // do some pre-processing stuff here
        // static Start() returns a new Process instance
        return Process.Start("C:\MyApp.exe", $"-option {processee.Option1}");
    }
}

到目前为止,我所做的是在这两种情况下都引入了一个我可以注入的抽象工厂类,它可以在需要时为我提供所需的东西(即在循环中动态地)。我有一个生产使用的实现。在测试代​​码中,我可以实现它或者只是模拟它(因为它是抽象的)。对于 Process 类,我引入了一个带有接口 (I)ProcessProxy 的代理类,它通过对实际 Process 类的调用传递。如果需要,我也可以轻松地嘲笑它。然后这两个示例变成了我在下面列出的内容。

我的问题是,在这两种情况下(这是我主要关心的问题),这是正确的方法吗?我知道我可能会引发固执己见的答案,但我只是想弄清楚这是否是本着干净直接的教科书(如 DependencyInjection 和 CompositionRoot 实现)的精神推荐的方式。如果这不是首选的方式,那是什么?

重构和启用 DI 后的循环示例:

public class MyClass
{
    private IVerifyer _verifyer;
    private AbstractDoerFactory _doerFactory;
    public MyClass(IVerifyer verifyer, AbstractDoerFactory doerFactory)
    {
        _verifyer = verifyer;
        _doerFactory = doerFactory;
    }

    public IList<Doer> PrepareDoingSomething(IList<IDoees> doees)
    {
        var doers = new List<Doer>();
        foreach (var doee in doees)
        {
            if (!_verifyer.Verify(doee)) throw new Exception("Blablabla...");
            doers.Add(_doerFactory.GetNewDoer(doee));
        }
        return doers;
    }
}

重构并启用 DI 后的流程示例:

public interface IProcessProxy : IDisposable
{
    TextReader StandardOutput { get; }
    TextReader StandardError { get; }
    int ExitCode { get; }
    Start(string fileName, string arguments);
    void Kill();
}

public class Processor
{
    private AbstractProcessProxyFactory _processProxyFactory;
    public Processor(AbstractProcessProxyFactory processProxyFactory)
    {
        _processProxyFactory = processProxyFactory;
    }

    public IProcessProxy ProcessSomething(IProcessee processee)
    {
        // do some pre-processing stuff here
        var processProxy = _processProxyFactory.GetProxyFactory();
        return processProxy.Start("C:\MyApp.exe", $"-option {processee.Option1}");
    }
}

【问题讨论】:

    标签: .net dependency-injection inversion-of-control ioc-container compositionroot


    【解决方案1】:

    DependencyInjection 是一个非常强大的工具,用于组织复杂的多态应用程序。我一直在寻找使用它的机会——但它并不适合处理应用程序的每个细节。

    虽然它可能只是出于示例目的,但我通常不会考虑使用 DI 来消除您对像 System.Process 这样的标准系统服务的依赖,除非您有充分的理由想要将其换成自定义流程类。这将是过度使用依赖注入的情况。

    也就是说,当我需要在引导过程之后动态构造类型的实例时,我发现工厂非常有用。为了获得最大的灵活性,不要依赖单个工厂实例。相反,提供为不同类型独立注册工厂的能力。

    public interface IFactory
    { 
        Type FactoryType;
        object CreateInstance(); 
    }
    
    namespace Generic {
        public interface IFactory<T> : IFactory
        {
            new T CreateInstance();
        }
    }
    
    public class DoThisFactory : IFactory<DoThis> { ... }
    public class DoThatFactory : IFactory<DoThat> { ... }
    
    
    public class MyClass
    {
        // use property injection
        public DoThisFactory DoThisFactory { get; set; }       
        public DoThatFactory DoThatFactory { get; set; }
    
        private DoThis _doThis = null;
        private DoThat _doThat = null;
    
        public DoThis DoThis
        {
            get {
                if(_doThis == null) _doThis = DoThisFactory.CreateInstance();
                return _doThis;
            }
        }
    
        public DoThat DoThat
        {
            get {
                if(_doThat == null) _doThat = DoThatFactory.CreateInstance();
                return _doThat;
            }
        }
    }
    

    在上面,MyClass 知道他需要做的不同类型的事情——依赖关系是明确的。这允许我为不同类型的 IDoees 提供不同(或相同)的工厂类。

    如果您不知道最终将使用的具体实现类型,那么您将需要注入一组 IFactory 实例并使用它们的 FactoryType 属性将它们注册到 Dictionary 中以供以后查找。

    public class MyClass
    {
        private Dictionary<Type, IFactory> _registerations = new Dictionary<Type, IFactory>();
    
        public MyClass(IFactory[] factories)
        {
            for(int i = 0; i < factories.Count -1; i++)
            {
                _registrations.Add(factories(i).FactoryType, factories);
            }
        }
    
        public IEnumeruable<IDoer> GetDoers(Type[] types)
        {
            List<IDoer> doers = new List<IDoer>();
            for(int i = 0; i < types.Count - 1; i++)
            {
                doers.Add(_registerations(types(i)).CreateInstance());
            }
            return doers;
        }
    }
    

    【讨论】:

      【解决方案2】:

      您可以解决工厂的第一个问题,如 OP 中所述,但问题是您是否应该这样做。从 OP 来看,尚不清楚 IDoees 是什么,但通常,作为方法参数传递或作为返回值返回的对象往往是 newables rather than injectables。那么,问题是你是否应该注入一个工厂。将Doer 简单化为new 可能更合适,如OP 中所示。

      当涉及到Process 之类的东西时,虽然您可以从技术上从具体类中提取接口,然后制作Adapter,但这通常不是使用依赖关系的最佳方式注射。

      根据Dependency Inversion Principle,抽象不应该依赖于细节;细节应该取决于抽象。 这通常意味着客户应该定义他们需要的抽象,然后实现应该匹配该抽象。

      因此,如果客户端必须运行一个进程,而不是调用它RunProcess,请根据它解决的问题命名它。例如,如果您想运行一个名为 encode.exe 的程序,例如要对一些音频文件进行编码,请根据该目标对依赖项进行建模:

      public class Foo
      {
          private readonly IEncoder encoder;
      
          public Foo(IEncoder encoder)
          {
              this.encoder = encoder;
          }
      
          public Bar DoSomething(Baz baz)
          {
              // Maybe do something else first...
      
              this.encoder.Encode(/*...*/);
      
              // Maybe do something else after...
          }
      }
      

      如果需要,您可以使用Process API 实现IEncoder:

      public class Encoder : IEncoder
      {
          public void Encode(/*...*/)
          {
              // Start encode.exe here...
          }
      }
      

      如果操作是长时间运行的,你可以从Encode返回一个Task,而不是void;如果您需要能够取消任务,请将CancellationToken 作为方法参数传递给Encode 方法;等等。围绕客户的需求建模依赖关系,而不是实现细节。

      【讨论】:

        猜你喜欢
        • 2021-08-29
        • 1970-01-01
        • 1970-01-01
        • 2019-03-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-04-01
        相关资源
        最近更新 更多