依赖注入和开发生产力 [英] Dependency Injection and development productivity

查看:37
本文介绍了依赖注入和开发生产力的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

摘要

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

设计挑战

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

依赖注入

出于多种原因,我选择采用 DI 方式.最明显的一个是可测试性,通过针对接口编程并通过构造函数向它们提供每个类的所有依赖项,这些类很容易测试,因为测试容器可以实例化所需的服务或它们的模拟,并输入每门课都要考.做 DI/IoC 的另一个原因是,不管你信不信,增加代码的可读性.不再需要大量的初始化过程来实例化所有不同的服务和手动实例化引用所需服务的类.配置内核(NInject)/注册表(StructureMap)可以方便地为引擎/游戏提供单点配置,在那里选择和配置服务实现.

我的问题

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

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

解决方案

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

<块引用>

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

如果您这样做,您可能违反了重用抽象原则 (RAP)

<块引用>

根据服务需求,一些类的构造函数可以得到非常大,这将使该类在其他情况下完全无用不使用 IoC 的上下文.

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

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

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

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

<块引用>

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

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

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

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

new SomeBusinessObject(new ThingsChangedNotificationService(new EmailErrorHandler()),新的电子邮件错误处理程序(),new MyDao(new EmailErrorHandler()));

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

公共静态类 SomeBusinessObjectFactory{公共静态 SomeBusinessObject Create(){返回新的 SomeBusinessObject(new ThingsChangedNotificationService(new EmailErrorHandler()),新的电子邮件错误处理程序(),new MyDao(new EmailErrorHandler()));}}

然后像这样使用它:

 SomeBusinessObject bo = SomeBusinessObjectFactory.Create();

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

public SomeBusinessObject(){var errorHandler = new EmailErrorHandler();var dao = new MyDao(errorHandler);var notificationService = new SomethingChangedNotificationService(errorHandler);初始化(notificationService, errorHandler, dao);}受保护的无效初始化(INotificationService 通知服务,IErrorHandler errorHandler,MyDao 道){this._NotificationService = 通知服务;this._ErrorHandler = errorHandler;this._Dao = dao;}

然后它看起来就像以前一样工作:

SomeBusinessObject bo = new SomeBusinessObject();

当您的默认实现在外部第三方库中时,使用Poor Man's DI 被认为是不好的,但如果您有一个好的默认实现,则不那么糟糕.

然后显然有所有的 DI 容器、对象构建器和其他模式.

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

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

公共接口 IDataAccessFactory{TDao创建<TDao>();}

正如您所看到的,这个版本是通用的,因为它可以利用 IoC 容器来创建各种类型(请注意,我的客户端仍然看不到 IoC 容器).

公共类 ConcreteDataAccessFactory : IDataAccessFactory{私有只读 IocContainer _Container;公共 ConcreteDataAccessFactory(IocContainer 容器){this._Container = 容器;}公共 TDao 创建(){返回 (TDao)Activator.CreateInstance(typeof(TDao),this._Container.Resolve(),this._Container.Resolve())}}

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

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

公共类 SomeOtherBusinessObject{私有 IDataAccessFactory _DataAccessFactory;公共 SomeOtherBusinessObject(IDataAccessFactory 数据访问工厂,INotificationService 通知服务,IErrorHandler errorHandler){this._DataAccessFactory = dataAccessFactory;}公共无效 DoSomething(){for (int i = 0; i <10; i++){使用 (var dao = this._DataAccessFactory.Create()){//使用 dao//Console.WriteLine(//"使用 dao:" + dao.GetHashCode().ToString());}}}}

<块引用>

基本上做 DI/IoC 会显着降低我的工作效率,并且在某些情况下,代码和架构进一步复杂化

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

松耦合和大图

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

什么是依赖关系?

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

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

哪些不是依赖,真的不需要 DI?

  • 列表
  • 内存流
  • 字符串/原语
  • 叶对象/Dto 的

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

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

Abstract

For the past few months I have been programming a light weight, C# based game engine with API abstraction and entity/component/scripting system. The whole idea of it is to ease the game development process in XNA, SlimDX and such, by providing architecture similar to that of the Unity engine.

Design challenges

As most game developers know, there are a lot of different services you need to access throughout your code. Many developers resort to using global static instances of e.g. a Render manager(or a composer), a Scene, Graphicsdevice(DX), Logger, Input state, Viewport, Window and so on. There are some alternative approaches to the global static instances/ singletons. One is to give each class an instance of the classes it needs access to, either through a constructor or constructor/property dependency injection(DI), another is to use a global service locator, like StructureMap's ObjectFactory where the service locator is usually configured as an IoC container.

Dependency Injection

I chose to go the DI way for many reasons. The most obvious one being testability, by programming against interfaces and have all the dependencies of every class provided to them through a constructor, those classes are easily tested since the test container can instantiate the required services, or the mocks of them, and feed into every class to be tested. Another reason for doing DI/IoC was, believe it or not, to increase the readability of the code. No more huge initialization process of instantiating all the different services and manually instantiating classes with references to the required services. Configuring the Kernel(NInject)/Registry(StructureMap) conveniently gives a single point of configuration for the engine/game, where service implementations are picked and configured.

My problems

  • I often feel like I am creating interfaces for interfaces sake
  • My productivity has gone down dramatically since all I do is worry about how to do things the DI-way, instead of the quick and simple global static way.
  • In some cases, e.g. when instantiating new Entities on runtime, one needs access to the IoC container / kernel to create the instance. This creates a dependency on the IoC container itself (ObjectFactory in SM, an instance of the kernel in Ninject), which really goes against the reason for using one in the first place. How can this be resolved? Abstract factories come to mind, but that just further complicates the code.
  • Depending on service requirements, some classes' constructors can get very large, which will make the class completely useless in other contexts where and if an IoC is not used.

Basically doing DI/IoC dramatically slows down my productivity and in some cases further complicates the code and architecture. Therefore I am uncertain of whether it is a path I should follow, or just give up and do things the old fashioned way. I am not looking for a single answer saying what I should or shouldn't do but a discussion on if using DI is worth it in the long run as opposed to using the global static/singleton way, possible pros and cons I have overlooked and possible solutions to my problems listed above, when dealing with DI.

解决方案

Should you go back to the old-fashioned way? My answer in short is no. DI has numerous benefits for all the reasons you mentioned.

I often feel like I am creating interfaces for interfaces sake

If you are doing this you might be violating the Reused Abstractions Principle (RAP)

Depending on service requirements, some classes' constructors can get very large, which will make the class completely useless in other contexts where and if an IoC is not used.

If your classes constructors are too large and complex, this is the best way to show you that you are violating a very important other principle: Single Reponsibility Principle. In this case it is time to extract and refactor your code into different classes, the number of dependencies suggested is around 4.

In order to do DI you don't have to have an interface, DI is just the way you get your dependencies into your object. Creating interfaces might be a needed way to be able to substitute a dependency for testing purposes. Unless the object of the dependency is:

  1. Easy to isolate
  2. Doesn't talk to external subsystems (file system etc)

You can create your dependency as an Abstract class, or any class where the methods you'd like to substitute are virtual. However interfaces do create the best de-coupled way of an dependency.

In some cases, e.g. when instantiating new Entities on runtime, one needs access to the IoC container / kernel to create the instance. This creates a dependency on the IoC container itself (ObjectFactory in SM, an instance of the kernel in Ninject), which really goes against the reason for using one in the first place. How can this be resolved? Abstract factories come to mind, but that just further complicates the code.

As far as a dependency to the IOC container, you should never have a dependency to it in your client classes. And they don't have to.

In order to first use dependency injection properly is to understand the concept of the Composition Root. This is the only place where your container should be referenced. At this point your entire object graph is constructed. Once you understand this you will realize you never need the container in your clients. As each client just gets its dependency injected.

There are also MANY other creational patterns you can follow to make construction easier: Say you want to construct an object with many dependencies like this:

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

You can create a concrete factory that knows how to construct this:

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

And then use it like this:

 SomeBusinessObject bo = SomeBusinessObjectFactory.Create();

You can also use poor mans di and create a constructor that takes no arguments at all:

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

Then it just seems like it used to work:

SomeBusinessObject bo = new SomeBusinessObject();

Using Poor Man's DI is considered bad when your default implementations are in external third party libraries, but less bad when you have a good default implementation.

Then obviously there are all the DI containers, Object builders and other patterns.

So all you need is to think of a good creational pattern for your object. Your object itself should not care how to create the dependencies, in fact it makes them MORE complicated and causes them to mix 2 kinds of logic. So I don't beleive using DI should have loss of productivity.

There are some special cases where your object cannot just get a single instance injected to it. Where the lifetime is generally shorter and on-the-fly instances are required. In this case you should inject the Factory into the object as a dependency:

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

As you can notice this version is generic because it can make use of an IoC container to create various types (Take note though the IoC container is still not visible to my client).

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>())
    }
}

Notice I used activator even though I had an Ioc container, this is important to note that the factory needs to construct a new instance of object and not just assume the container will provide a new instance as the object may be registered with different lifetimes (Singleton, ThreadLocal, etc). However depending on which container you are using some can generate these factories for you. However if you are certain the object is registered with Transient lifetime, you can simply resolve it.

EDIT: Adding class with Abstract Factory dependency:

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

Basically doing DI/IoC dramatically slows down my productivity and in some cases further complicates the code and architecture

Mark Seeman wrote an awesome blog on the subject, and answered the question: My first reaction to that sort of question is: you say loosely coupled code is harder to understand. Harder than what?

Loose Coupling and the Big Picture

EDIT: Finally I'd like to point out that not every object and dependency needs or should be dependency injected, first consider if what you are using is actually considered a dependency:

What are dependencies?

  • Application Configuration
  • System Resources (Clock)
  • Third Party Libraries
  • Database
  • WCF/Network Services
  • External Systems (File/Email)

Any of the above objects or collaborators can be out of your control and cause side effects and difference in behavior and make it hard to test. These are the times to consider an Abstraction (Class/Interface) and use DI.

What are not dependencies, doesn't really need DI?

  • List<T>
  • MemoryStream
  • Strings/Primitives
  • Leaf Objects/Dto's

Objects such as the above can simply be instantiated where needed using the new keyword. I would not suggest using DI for such simple objects unless there are specific reasons. Consider the question if the object is under your full control and doesn't cause any additional object graphs or side effects in behavior (at least anything that you want to change/control the behavior of or test). In this case simply new them up.

I have posted a lot of links to Mark Seeman's posts, but I really recommend you read his book and blog posts.

这篇关于依赖注入和开发生产力的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆