草庐IT

c# - 依赖注入(inject)和开发生产力

coder 2024-05-29 原文

摘要

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

设计挑战

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

依赖注入(inject)

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

我的问题

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

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

    最佳答案

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

    I often feel like I am creating interfaces for interfaces sake



    如果您这样做,您可能会违反
    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.



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

    为了进行 DI,您不必拥有接口(interface),DI 只是您将依赖项放入对象的方式。创建接口(interface)可能是一种能够替代依赖项以进行测试的必要方式。
    除非依赖的对象是:
  • 易于隔离
  • 不与外部子系统(文件系统
    等)

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

    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.



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

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

    您还可以遵循许多其他创建模式来使构建更容易:
    假设您要构造一个具有许多依赖项的对象,如下所示:
    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();
    

    您还可以使用poor 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 容器、对象构建器和其他模式。

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

    在某些特殊情况下,您的对象不能只注入(inject)单个实例。生命周期通常较短且需要动态实例的地方。在这种情况下,您应该将 Factory 作为依赖项注入(inject)对象:
    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 容器,我也使用了激活器,重要的是要注意工厂需要构造一个新的对象实例,而不仅仅是假设容器将提供一个新实例,因为该对象可能注册了不同的生命周期(单例、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());
                }
            }
        }
    }
    

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



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

    Loose Coupling and the Big Picture

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

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

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

    什么不是依赖项,真的不需要 DI?
  • List<T>
  • 内存流
  • 字符串/原语
  • 叶对象/Dto's

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

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

    关于c# - 依赖注入(inject)和开发生产力,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10313507/

    有关c# - 依赖注入(inject)和开发生产力的更多相关文章

    1. ruby - 使用 C 扩展开发 ruby​​gem 时,如何使用 Rspec 在本地进行测试? - 2

      我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当

    2. ruby-on-rails - 在 ruby​​ .gemspec 文件中,如何指定依赖项的多个版本? - 2

      我正在尝试修改当前依赖于定义为activeresource的gem:s.add_dependency"activeresource","~>3.0"为了让gem与Rails4一起工作,我需要扩展依赖关系以与activeresource的版本3或4一起工作。我不想简单地添加以下内容,因为它可能会在以后引起问题:s.add_dependency"activeresource",">=3.0"有没有办法指定可接受版本的列表?~>3.0还是~>4.0? 最佳答案 根据thedocumentation,如果你想要3到4之间的所有版本,你可以这

    3. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

      我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

    4. Ruby Sinatra 配置用于生产和开发 - 2

      我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm

    5. ruby - 是否可以覆盖 gemfile 进行本地开发? - 2

      我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI

    6. c# - 如何在 ruby​​ 中调用 C# dll? - 2

      如何在ruby​​中调用C#dll? 最佳答案 我能想到几种可能性:为您的DLL编写(或找人编写)一个COM包装器,如果它还没有,则使用Ruby的WIN32OLE库来调用它;看看RubyCLR,其中一位作者是JohnLam,他继续在Microsoft从事IronRuby方面的工作。(估计不会再维护了,可能不支持.Net2.0以上的版本);正如其他地方已经提到的,看看使用IronRuby,如果这是您的技术选择。有一个主题是here.请注意,最后一篇文章实际上来自JohnLam(看起来像是2009年3月),他似乎很自在地断言RubyCL

    7. ruby - 在 Windows 机器上使用 Ruby 进行开发是否会适得其反? - 2

      这似乎非常适得其反,因为太多的gem会在window上破裂。我一直在处理很多mysql和ruby​​-mysqlgem问题(gem本身发生段错误,一个名为UnixSocket的类显然在Windows机器上不能正常工作,等等)。我只是在浪费时间吗?我应该转向不同的脚本语言吗? 最佳答案 我在Windows上使用Ruby的经验很少,但是当我开始使用Ruby时,我是在Windows上,我的总体印象是它不是Windows原生系统。因此,在主要使用Windows多年之后,开始使用Ruby促使我切换回原来的系统Unix,这次是Linux。Rub

    8. ruby-on-rails - 在 Rails 开发环境中为 .ogv 文件设置 Mime 类型 - 2

      我正在玩HTML5视频并且在ERB中有以下片段:mp4视频从在我的开发环境中运行的服务器很好地流式传输到chrome。然而firefox显示带有海报图像的视频播放器,但带有一个大X。问题似乎是mongrel不确定ogv扩展的mime类型,并且只返回text/plain,如curl所示:$curl-Ihttp://0.0.0.0:3000/pr6.ogvHTTP/1.1200OKConnection:closeDate:Mon,19Apr201012:33:50GMTLast-Modified:Sun,18Apr201012:46:07GMTContent-Type:text/plain

    9. ruby-on-rails - 在 Rails 中调试生产服务器 - 2

      您如何在Rails中的实时服务器上进行有效调试,无论是在测试版/生产服务器上?我试过直接在服务器上修改文件,然后重启应用,但是修改好像没有生效,或者需要很长时间(缓存?)我也试过在本地做“脚本/服务器生产”,但是那很慢另一种选择是编码和部署,但效率很低。有人对他们如何有效地做到这一点有任何见解吗? 最佳答案 我会回答你的问题,即使我不同意这种热修补服务器代码的方式:)首先,你真的确定你已经重启了服务器吗?您可以通过跟踪日志文件来检查它。您更改的代码显示的View可能会被缓存。缓存页面位于tmp/cache文件夹下。您可以尝试手动删除

    10. C# 到 Ruby sha1 base64 编码 - 2

      我正在尝试在Ruby中复制Convert.ToBase64String()行为。这是我的C#代码:varsha1=newSHA1CryptoServiceProvider();varpasswordBytes=Encoding.UTF8.GetBytes("password");varpasswordHash=sha1.ComputeHash(passwordBytes);returnConvert.ToBase64String(passwordHash);//returns"W6ph5Mm5Pz8GgiULbPgzG37mj9g="当我在Ruby中尝试同样的事情时,我得到了相同sha

    随机推荐