依赖注入和开发生产力

edv*_*dig 11 c# structuremap architecture dependency-injection ninject

抽象

在过去的几个月里,我一直在编写一个轻量级,基于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时,上面列出的问题的可能解决方案.

And*_*dre 20

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

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

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

根据服务要求,某些类的构造函数可能变得非常大,这将使该类在其他情况下完全无用,如果不使用IoC.

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

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

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

您可以将依赖项创建为Abstract类,或者您要替换的方法是虚拟的任何类.但是,接口确实创建了依赖关系的最佳解耦方式.

在某些情况下,例如在运行时实例化新实体时,需要访问IoC容器/内核来创建实例.这会产生对IoC容器本身的依赖(SM中的ObjectFactory,Ninject中的内核实例),这实际上违背了首先使用它的原因.怎么解决这个问题?我想到了抽象工厂,但这进一步使代码复杂化.

就IOC容器的依赖性而言,在客户端类中永远不应该依赖它.他们没有必要.

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

您还可以使用许多其他创建模式来简化构建:假设您要构建具有许多依赖关系的对象,如下所示:

new SomeBusinessObject(
    new SomethingChangedNotificationService(new EmailErrorHandler()),
    new EmailErrorHandler(),
    new MyDao(new EmailErrorHandler()));
Run Code Online (Sandbox Code Playgroud)

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

public static class SomeBusinessObjectFactory
{
    public static SomeBusinessObject Create()
    {
        return new SomeBusinessObject(
            new SomethingChangedNotificationService(new EmailErrorHandler()),
            new EmailErrorHandler(),
            new MyDao(new EmailErrorHandler()));
    }
}
Run Code Online (Sandbox Code Playgroud)

然后像这样使用它:

 SomeBusinessObject bo = SomeBusinessObjectFactory.Create();
Run Code Online (Sandbox Code Playgroud)

你也可以使用差的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;
}
Run Code Online (Sandbox Code Playgroud)

然后它似乎曾经工作过:

SomeBusinessObject bo = new SomeBusinessObject();
Run Code Online (Sandbox Code Playgroud)

当你的默认实现在外部第三方库中时,使用穷人的DI被认为是错误的,但是当你有一个好的默认实现时,则会更糟.

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

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

在某些特殊情况下,您的对象无法仅将单个实例注入其中.通常寿命较短且需要实时的实例.在这种情况下,您应该将Factory作为依赖项注入对象:

public interface IDataAccessFactory
{
    TDao Create<TDao>();
}
Run Code Online (Sandbox Code Playgroud)

您可以注意到这个版本是通用的,因为它可以使用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>())
    }
}
Run Code Online (Sandbox Code Playgroud)

注意我使用了激活器,即使我有一个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());
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

松耦合和大图

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

什么是依赖?

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

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

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

  • List<T>
  • 的MemoryStream
  • 字符串/基元
  • Leaf Objects/Dto's

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

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