11

抽象的

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

4

1 回答 1

20

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

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

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

根据服务要求,某些类的构造函数可能会变得非常大,这将使该类在不使用 IoC 的其他上下文中完全无用。

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

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

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

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

在某些情况下,例如在运行时实例化新实体时,需要访问 IoC 容器/内核来创建实例。这会创建对 IoC 容器本身的依赖(SM 中的 ObjectFactory,Ninject 中的内核实例),这确实与最初使用 IoC 容器的原因背道而驰。如何解决?抽象工厂浮现在脑海中,但这只会使代码更加复杂。

至于对 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's 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 等)。但是,根据您使用的容器,有些可以为您生成这些工厂。但是,如果您确定对象已注册瞬态生命周期,则可以简单地解决它。

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

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

松散耦合和全局

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

什么是依赖项?

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

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

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

  • List<T>
  • 记忆流
  • 字符串/原语
  • 叶对象/Dto

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

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

于 2012-04-25T11:33:04.983 回答