【问题标题】:Dependency Injection and development productivity依赖注入和开发生产力
【发布时间】:2012-04-25 10:07:41
【问题描述】:

摘要

在过去的几个月里,我一直在编写一个轻量级的、基于 C# 的游戏引擎,它具有 API 抽象和实体/组件/脚本系统。它的整个想法是通过提供类似于 Unity 引擎的架构来简化 XNA、SlimDX 等的游戏开发过程。

设计挑战

正如大多数游戏开发人员所知,您需要在整个代码中访问许多不同的服务。许多开发人员求助于使用全局静态实例,例如渲染管理器(或作曲家)、场景、图形设备(DX)、记录器、输入状态、视口、窗口等。全局静态实例/单例有一些替代方法。一种是通过构造函数或构造函数/属性依赖注入 (DI) 为每个类提供它需要访问的类的实例,另一种是使用全局服务定位器,如 StructureMap 的 ObjectFactory,其中服务定位器通常配置为一个 IoC 容器。

依赖注入

出于多种原因,我选择了 DI 方式。最明显的一点是可测试性,通过针对接口进行编程并通过构造函数向它们提供每个类的所有依赖项,这些类很容易测试,因为测试容器可以实例化所需的服务或它们的模拟,并输入每一堂课都要考。信不信由你,进行 DI/IoC 的另一个原因是为了提高代码的可读性。不再需要实例化所有不同服务和手动实例化所需服务的类的巨大初始化过程。配置 Kernel(NInject)/Registry(StructureMap) 可以方便地为引擎/游戏提供单点配置,其中选择和配置服务实现。

我的问题

  • 我经常觉得我是为了接口而创建接口
  • 我的工作效率急剧下降,因为我所做的只是担心如何以 DI 方式做事,而不是快速简单的 全局静态 方式。
  • 在某些情况下,例如在运行时实例化新实体时,需要访问 IoC 容器/内核来创建实例。这会创建对 IoC 容器本身的依赖(SM 中的 ObjectFactory,Ninject 中的内核实例),这确实违背了最初使用它的原因。如何解决?想到了抽象工厂,但这只会使代码更加复杂。
  • 根据服务要求,某些类的构造函数可能会变得非常大,这将使该类在不使用 IoC 的其他上下文中完全无用。

基本上执行 DI/IoC 会大大降低我的工作效率,并且在某些情况下会使代码和架构更加复杂。因此,我不确定这是我应该遵循的道路,还是放弃并以老式的方式做事。我不是在寻找一个单一的答案来说明我应该做什么或不应该做什么,而是讨论从长远来看使用 DI 是否值得,而不是使用全局静态/单例方式,我忽略了可能的利弊和在处理 DI 时,我上面列出的问题的可能解决方案。

【问题讨论】:

    标签: c# dependency-injection architecture ninject structuremap


    【解决方案1】:

    你应该回到老式的方式吗? 简而言之,我的回答是否定的。由于您提到的所有原因,DI 有很多好处。

    我经常觉得我是为了接口而创建接口

    如果您这样做,您可能违反了 Reused Abstractions Principle (RAP)

    根据服务需求,一些类的构造函数可以获取 非常大,这将使该类在其他方面完全无用 不使用 IoC 以及是否使用 IoC 的上下文。

    如果您的类构造函数太大且太复杂,这是向您表明您违反了另一个非常重要的其他原则的最佳方式: Single Reponsibility Principle。在这种情况下,是时候将代码提取并重构为不同的类了,建议的依赖项数量约为 4。

    为了进行 DI,您不必有接口,DI 只是您将依赖项放入对象的方式。创建接口可能是能够替代依赖项以进行测试的必要方法。 除非依赖的对象是:

    1. 易于隔离
    2. 不与外部子系统(文件系统 等)

    您可以将依赖项创建为抽象类,或者您希望替换的方法是虚拟的任何类。然而,接口确实创建了最佳的依赖解耦方式。

    在某些情况下,例如在运行时实例化新实体时,一个 需要访问 IoC 容器/内核来创建实例。 这会创建对 IoC 容器本身的依赖(ObjectFactory 在 SM 中,Ninject 中的内核实例) 反对首先使用一个的原因。怎么会这样 解决?抽象工厂浮现在脑海中,但这只是进一步 使代码复杂化。

    就对 IOC 容器的依赖而言,您永远不应该在客户端类中对它有依赖。 他们不必这样做。

    要正确使用依赖注入首先要了解Composition Root的概念。这是唯一应该引用您的容器的地方。至此,您的整个对象图已构建完毕。一旦你理解了这一点,你就会意识到你的客户端永远不需要容器。因为每个客户端都只是注入了它的依赖项。

    您还可以遵循许多其他创建模式来简化构建: 假设您要构造一个具有许多依赖项的对象,如下所示:

    new SomeBusinessObject(
        new SomethingChangedNotificationService(new EmailErrorHandler()),
        new EmailErrorHandler(),
        new MyDao(new EmailErrorHandler()));
    

    您可以创建一个知道如何构造它的具体工厂:

    public static class SomeBusinessObjectFactory
    {
        public static SomeBusinessObject Create()
        {
            return new SomeBusinessObject(
                new SomethingChangedNotificationService(new EmailErrorHandler()),
                new EmailErrorHandler(),
                new MyDao(new EmailErrorHandler()));
        }
    }
    

    然后像这样使用它:

     SomeBusinessObject bo = SomeBusinessObjectFactory.Create();
    

    您也可以使用可怜的 mans di 并创建一个完全不带参数的构造函数:

    public SomeBusinessObject()
    {
        var errorHandler = new EmailErrorHandler();
        var dao = new MyDao(errorHandler);
        var notificationService = new SomethingChangedNotificationService(errorHandler);
        Initialize(notificationService, errorHandler, dao);
    }
    
    protected void Initialize(
        INotificationService notifcationService,
        IErrorHandler errorHandler,
        MyDao dao)
    {
        this._NotificationService = notifcationService;
        this._ErrorHandler = errorHandler;
        this._Dao = dao;
    }
    

    然后它看起来就像它曾经工作过:

    SomeBusinessObject bo = new SomeBusinessObject();
    

    当您的默认实现位于外部第三方库中时,使用 Poor Man 的 DI 被认为是不好的,但如果您有一个好的默认实现,那么不好。

    那么显然有所有的 DI 容器、对象构建器和其他模式。

    所以你所需要的只是为你的对象想一个好的创建模式。您的对象本身不应该关心如何创建依赖项,实际上它使它们更加复杂并导致它们混合了两种逻辑。所以我不相信使用 DI 会降低生产力。

    在某些特殊情况下,您的对象不能只将单个实例注入其中。生命周期通常较短并且需要动态实例。在这种情况下,您应该将 Factory 作为依赖项注入到对象中:

    public interface IDataAccessFactory
    {
        TDao Create<TDao>();
    }
    

    您可以注意到这个版本是通用的,因为它可以利用 IoC 容器来创建各种类型(请注意,尽管 IoC 容器对我的客户端仍然不可见)。

    public class ConcreteDataAccessFactory : IDataAccessFactory
    {
        private readonly IocContainer _Container;
    
        public ConcreteDataAccessFactory(IocContainer container)
        {
            this._Container = container;
        }
    
        public TDao Create<TDao>()
        {
            return (TDao)Activator.CreateInstance(typeof(TDao),
                this._Container.Resolve<Dependency1>(), 
                this._Container.Resolve<Dependency2>())
        }
    }
    

    请注意,即使我有一个 Ioc 容器,我也使用了 activator,请务必注意,工厂需要构造一个新的对象实例,而不仅仅是假设容器将提供一个新实例,因为该对象可能注册了不同的生命周期(Singleton、ThreadLocal 等)。但是,根据您使用的容器,有些可以为您生成这些工厂。但是,如果您确定该对象已注册到 Transient 生命周期,则可以简单地解决它。

    编辑:添加具有抽象工厂依赖项的类:

    public class SomeOtherBusinessObject
    {
        private IDataAccessFactory _DataAccessFactory;
    
        public SomeOtherBusinessObject(
            IDataAccessFactory dataAccessFactory,
            INotificationService notifcationService,
            IErrorHandler errorHandler)
        {
            this._DataAccessFactory = dataAccessFactory;
        }
    
        public void DoSomething()
        {
            for (int i = 0; i < 10; i++)
            {
                using (var dao = this._DataAccessFactory.Create<MyDao>())
                {
                    // work with dao
                    // Console.WriteLine(
                    //     "Working with dao: " + dao.GetHashCode().ToString());
                }
            }
        }
    }
    

    基本上执行 DI/IoC 会大大降低我的工作效率,并且 某些情况会使代码和架构进一步复杂化

    Mark Seeman 就这个主题写了一篇很棒的博客,并回答了这个问题: 我对这类问题的第一反应是:你说松散耦合的代码更难理解。比什么更难?

    Loose Coupling and the Big Picture

    编辑:最后我想指出,并非每个对象和依赖项都需要或应该被依赖注入,首先考虑您使用的内容是否实际上被视为依赖项:

    什么是依赖关系?

    • 应用程序配置
    • 系统资源(时钟)
    • 第三方库
    • 数据库
    • WCF/网络服务
    • 外部系统(文件/电子邮件)

    上述任何对象或协作者都可能超出您的控制范围,并导致副作用和行为差异,并使其难以测试。现在是考虑抽象(类/接口)并使用 DI 的时候了。

    什么不是依赖,真的不需要 DI?

    • List&lt;T&gt;
    • 内存流
    • 字符串/原语
    • 叶对象/Dto 的

    可以在需要的地方使用new 关键字简单地实例化上述对象。除非有特殊原因,否则我不建议将 DI 用于此类简单的对象。考虑对象是否在您的完全控制之下并且不会导致任何额外的对象图或行为副作用(至少是您想要更改/控制行为或测试的任何东西)的问题。在这种情况下,只需更新它们即可。

    我已经发布了很多指向 Mark Seeman 帖子的链接,但我真的建议您阅读他的书和博客文章。

    【讨论】:

    • 很好的回复,正题,谢谢!我阅读了您链接的两篇文章,它们都是很好的信息来源,实际上回答了我想到的许多其他问题。在我的案例中,RAP 违规和 SRP 是值得牢记的好东西。最后一件事从您的回复中脱颖而出,您提到使用抽象工厂向您的客户隐藏 IoC 容器,那么抽象工厂可以依赖 IoC 容器吗?
    • 谢谢,我很高兴这对你有所帮助,我会说是的,抽象工厂了解容器是非常可接受的,因为工厂与组合根在同一层中运行“胶水”,它不包含业务逻辑,它仅控制具体实例。因此,在测试环境中,您甚至可能不使用 Ioc 容器,并且无论如何您都需要不同的工厂,因为具体类型会不同
    • 嗨,我刚刚在最后添加了另一个编辑,让您了解什么是依赖项,而不用担心那些简单且不需要它的对象的任何依赖项管理。
    • +1 我还要补充一点,很容易自欺欺人地认为给定 ID 容器中 Scope 的复杂性以及所有能感觉到它把你拖入泥潭的废话都是容器的错。它不是 - 它是 [Your Design of] 你的应用程序所固有的。最好将它放在一个组合根中,其中概念有名称并且可以根据需要重新调整,而不是在这个地方编写大量的工厂代码。如果没有 a) 阅读 @Mark Seemann 的评分最高的帖子和 b) 购买 manning.com/seemann,请不要浪费时间学习 DI
    • 感谢安德烈,非常感谢。并感谢这些指针@Ruben Bartelink
    猜你喜欢
    • 2018-04-28
    • 1970-01-01
    • 1970-01-01
    • 2019-10-28
    • 1970-01-01
    • 2021-11-19
    • 2019-04-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多