依赖注入和开发生产力

时间:2012-04-25 10:07:41

标签: c# dependency-injection architecture ninject structuremap

抽象

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

设计挑战

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

依赖注入

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

我的问题

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

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

1 个答案:

答案 0 :(得分:20)

你应该回到老式的方式吗? 我的答案总之是否定的。由于你提到的所有原因,DI有许多好处。

  

我经常觉得我正在为接口创建接口

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

  

根据服务要求,某些课程'建设者可以得到   非常大,这将使该类在其他方面完全无用   上下文中不使用IoC的地方。

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

为了做DI,你不必拥有一个界面,DI就是让你的依赖进入对象的方式。创建接口可能是能够将依赖项替换为测试目的的必要方式。 除非依赖的对象是:

  1. 易于分离
  2. 不与外部子系统(文件系统)通信 等)
  3. 您可以将依赖项创建为Abstract类,或者您想要替换的方法是虚拟的任何类。然而,接口确实创建了依赖的最佳解耦方式。

      

    在某些情况下,例如在运行时实例化新实体时,一个   需要访问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();
    

    当你的默认实现在外部第三方库中时,使用穷人的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容器,重要的是要注意工厂需要构造一个新的对象实例而不只是假设容器将提供一个新实例,因为该对象可能注册不同生命周期(Singleton,ThreadLocal等)。但是,根据您使用的容器,有些可以为您生成这些工厂。但是,如果您确定该对象已在Transient生命周期中注册,则可以直接解析它。

    编辑:使用Abstract Factory依赖项添加类:

    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会大大降低我的工作效率   某些情况使代码和架构更加复杂化

    马克·西曼写了一篇关于这个主题的精彩博客,回答了这个问题: 我对这类问题的第一反应是:你说松散耦合的代码更难理解。比什么更难?

    Loose Coupling and the Big Picture

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

    什么是依赖项?

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

    任何上述对象或协作者都可能无法控制并导致副作用和行为差异,并使其难以测试。这些是考虑抽象(类/接口)并使用DI的时候。

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

    • List<T>
    • MemoryStream
    • 字符串/基元
    • Leaf Objects / Dto&#39;

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

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