依赖注入和开发效率

2024-01-09

Abstract

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

设计挑战

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

依赖注入

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

我的问题

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

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


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

我经常觉得我是为了界面而创建界面

如果您这样做,您可能违反了重用抽象原则 (RAP) http://www.codemanship.co.uk/parlezuml/blog/?postid=934

根据服务需求,某些类的构造函数可以获得 非常大,这将使该类在其他方面完全无用 未使用 IoC 的上下文。

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

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

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

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

在某些情况下,例如在运行时实例化新实体时,一 需要访问 IoC 容器/内核才能创建实例。 这会创建对 IoC 容器本身(ObjectFactory 在 SM 中,Ninject 中的内核实例),这确实是 反对首先使用它的原因。怎么会这样 解决?我想到了抽象工厂,但这还只是更进一步 使代码变得复杂。

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

为了正确使用依赖注入,首先要理解依赖注入的概念成分根 http://blog.ploeh.dk/2011/07/28/CompositionRoot.aspx。这是唯一应该引用您的容器的地方。至此,整个对象图就构建完成了。一旦你理解了这一点,你就会意识到你的客户端永远不需要容器。因为每个客户端都只是注入其依赖项。

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

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

您还可以使用 bad 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 的 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,值得注意的是,工厂需要构造一个新的对象实例,而不仅仅是假设容器将提供一个新实例,因为该对象可能会注册不同的生命周期(单例) 、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());
            }
        }
    }
}

基本上,进行 DI/IoC 会极大地降低我的工作效率,并且 有些情况使代码和架构进一步复杂化

马克·西曼(Mark Seeman)就此主题写了一篇很棒的博客,并回答了这个问题: 我对这类问题的第一反应是:你说松散耦合的代码更难理解。比什么更难?

松耦合和大局 http://blog.ploeh.dk/2012/02/02/LooseCouplingAndTheBigPicture.aspx

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

什么是依赖关系?

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

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

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

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

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

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

依赖注入和开发效率 的相关文章

  • 没有强命名的代码签名是否会让您的应用程序容易被滥用?

    尝试了解authenticode代码签名和强命名 我是否正确地认为 如果我对引用一些 dll 非强命名 的 exe 进行代码签名 恶意用户就可以替换我的 DLL 并以看似由我签名但正在运行的方式分发应用程序他们的代码 假设这是真的 那么您似
  • 我如何才能等待多个事情

    我正在使用 C 11 和 stl 线程编写一个线程安全队列 WaitAndPop 方法当前如下所示 我希望能够将一些内容传递给 WaitAndPop 来指示调用线程是否已被要求停止 如果 WaitAndPop 等待并返回队列的元素 则应返回
  • 按成员序列化

    我已经实现了template
  • 秒表有最长运行时间吗?

    多久可以Stopwatch在 NET 中运行 如果达到该限制 它会回绕到负数还是从 0 重新开始 Stopwatch Elapsed返回一个TimeSpan From MSDN https learn microsoft com en us
  • 查找c中结构元素的偏移量

    struct a struct b int i float j x struct c int k float l y z 谁能解释一下如何找到偏移量int k这样我们就可以找到地址int i Use offsetof 找到从开始处的偏移量z
  • Asp.NET WebApi 中类似文件名称的路由

    是否可以在 ASP NET Web API 路由配置中添加一条路由 以允许处理看起来有点像文件名的 URL 我尝试添加以下条目WebApiConfig Register 但这不起作用 使用 URIapi foo 0de7ebfa 3a55
  • BitTorrent 追踪器宣布问题

    我花了一点业余时间编写 BitTorrent 客户端 主要是出于好奇 但部分是出于提高我的 C 技能的愿望 我一直在使用理论维基 http wiki theory org BitTorrentSpecification作为我的向导 我已经建
  • Clang 3.1 + libc++ 编译错误

    我已经构建并安装了 在前缀下 alt LLVM Clang trunk 2012 年 4 月 23 日 在 Ubuntu 12 04 上成功使用 GCC 4 6 然后使用此 Clang 构建的 libc 当我想使用它时我必须同时提供 lc
  • 关于 C++ 转换:参数 1 从“[some_class]”到“[some_class]&”没有已知的转换

    我正在研究 C 并且遇到了一个错误 我不知道确切的原因 我已经找到了解决方案 但仍然想知道原因 class Base public void something Base b int main Base b b something Base
  • 不同枚举类型的范围和可转换性

    在什么条件下可以从一种枚举类型转换为另一种枚举类型 让我们考虑以下代码 include
  • 堆栈溢出:堆栈空间中重复的临时分配?

    struct MemBlock char mem 1024 MemBlock operator const MemBlock b const return MemBlock global void foo int step 0 if ste
  • C++ OpenSSL 导出私钥

    到目前为止 我成功地使用了 SSL 但遇到了令人困惑的障碍 我生成了 RSA 密钥对 之前使用 PEM write bio RSAPrivateKey 来导出它们 然而 手册页声称该格式已经过时 实际上它看起来与通常的 PEM 格式不同 相
  • 如何设计以 char* 指针作为类成员变量的类?

    首先我想介绍一下我的情况 我写了一些类 将 char 指针作为私有类成员 而且这个项目有 GUI 所以当单击按钮时 某些函数可能会执行多次 这些类是设计的单班在项目中 但是其中的某些函数可以执行多次 然后我发现我的项目存在内存泄漏 所以我想
  • 对现有视频添加水印

    我正在寻找一种用 C 在视频上加水印的方法 就像在上面写文字一样 图片或文字标签 我该怎么做 谢谢 您可以使用 Nreco 视频转换器 代码看起来像 NReco VideoConverter FFMpegConverter wrap new
  • 为什么编译时浮点计算可能不会得到与运行时计算相同的结果?

    In the speaker mentioned Compile time floating point calculations might not have the same results as runtime calculation
  • C# 成员变量继承

    我对 C 有点陌生 但我在编程方面有相当广泛的背景 我想做的事情 为游戏定义不同的 MapTiles 我已经像这样定义了 MapTile 基类 public class MapTile public Texture2D texture pu
  • C# 模拟VolumeMute按下

    我得到以下代码来模拟音量静音按键 DllImport coredll dll SetLastError true static extern void keybd event byte bVk byte bScan int dwFlags
  • IEnumreable 动态和 lambda

    我想在 a 上使用 lambda 表达式IEnumerable
  • 哪种 C 数据类型可以表示 40 位二进制数?

    我需要表示一个40位的二进制数 应该使用哪种 C 数据类型来处理这个问题 如果您使用的是 C99 或 C11 兼容编译器 则使用int least64 t以获得最大的兼容性 或者 如果您想要无符号类型 uint least64 t 这些都定
  • 使用.NET技术录制屏幕视频[关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 有没有一种方法可以使用 NET 技术来录制屏幕 无论是桌面还是窗口 我的目标是免费的 我喜欢小型 低

随机推荐

  • 连接 pandas 中的列表 - 使用 PyCharm 发出警告

    这是一个最小的可重现示例 用于获取我不明白的警告 我的数据框 前 5 行 如下所示 10 列 每列都填充了一个字符串列表 Index HLA A1 D HLA A2 D HLA B1 D HLA B2 D HLA C1 D HLA C2 D
  • 在“获取源”步骤之后,TFS 保持不同步

    We migrated to TFS 2015 RTM recently and were successful in creating build pools configuring build agents and build defi
  • 创建静态库

    我正在尝试创建一个静态库以在我的 PHP 扩展中使用 为此 我正在编译我的 c文件使用gcc c file c o file o并获得 o文件 然后我用ar rcs lib a o将所有编译的对象归档到 a file 完成此操作后 我指的是
  • haskell负十进制数[重复]

    这个问题在这里已经有答案了 可能的重复 Haskell 中的负双精度数或浮点数 macports https stackoverflow com questions 4101599 negative doubles or floats in
  • 表单验证如何排除输入字段?

    我正在关注 bootstrap 4 表单验证https getbootstrap com docs 4 0 components forms validation https getbootstrap com docs 4 0 compon
  • 如何在 Context.MODE_PRIVATE 中创建嵌套文件夹和文件?

    我有一个要求 需要使用嵌套结构编写文件和文件夹Context MODE PRIVATE 我发现我们可以使用创建文件openFileOutput FILENAME Context MODE PRIVATE 并能够使用此方法创建文件 但后来我发
  • 如何从我的应用程序中启动 Mail.app 中的新消息窗口

    我可以使用命令启动 Mail app NSWorkspace共享工作空间 launchApplication Mail app 但我想在 Mail app 中启动新消息窗口 而不是整个 Mail app 我怎样才能这样做呢 我得到了答案 N
  • 从文件返回细节,python

    我有这段代码 我正在尝试计算以下内容的数量 py 脚本中的代码行 for loops 对于 while loops 同时 if 语句 如果 函数定义 def 乘号 除号 加号 减号 在数学符号上 代码可以工作 但是当代码寻找 if 语句时
  • 如何使用cmd/批处理文件删除目录中名为x的所有文件夹

    我有一个名为 x 的文件夹 其中包含许多子文件夹和文件 我想删除 x 中存在的名为 y 的文件夹及其所有子文件夹 必须删除的所述文件夹可能包含也可能不包含任何文件 我相信我可以使用 cmd 或某种批处理文件来完成此操作 但我是一个命令行新人
  • 在位图样式设计器中更改字体

    Delphi XE7 提供了 位图样式设计器 工具 工具 gt 位图样式设计器 可用于为您的 Metro 主题应用程序编辑和创建样式 更改按钮 复选框和标签的图形和颜色很有效 而且看起来很漂亮 但如何更改字体设置呢 更准确地说 我该怎么做才
  • 比亚恩会犯错误吗? (一边解释模板),还是我还是不明白?

    伙计们 我正在做 C 编程语言第三版 的练习 第 340 页有一个函数示例 template
  • 确定文件是否为空(SSIS)

    我正在尝试在 SSIS 2005 中开发一个包 我的过程的一部分是检查网络上的文件是否为空 如果不为空 则需要传递成功状态 否则 需要传递不成功状态 我想我需要一个脚本任务 但不知道如何去做 任何帮助表示赞赏 Create a connec
  • 保留 Emacs 中的窗口布局

    我已经以某种方式设置了我的窗口 如何保存此设置以供以后调用 我有时还是用C x r w
  • 验证本地 Laravel Homestead 服务器上的自签名证书

    我按照以下详细信息创建了 SSL 证书 因此我可以使用 https 通过 Laravel 的 Homestead 运行本地测试站点 在 homestead 虚拟机上添加 https 证书 https stackoverflow com qu
  • UITableViewController 背景图片

    如何设置图像UITableViewController 我使用了很多东西 但它不能帮助我将图像设置为背景 UIImageView bgView UIImageView alloc initWithImage UIImage imageNam
  • 在 r 绘图文本中指定小数位?

    我尝试格式化在基本图形系统中创建的回归曲线的标签 基本上 该标签从变量中提取斜率 截距和 r 方值 示例如下 plot rnorm 10 type n xlim c 0 100 ylim c 0 100 text x 0 y 100 adj
  • VBA许多按钮指向同一个_Click sub

    我的表单上有一堆文本框按钮对 单击按钮时 我想将文本框的值插入数据库 名称 文本框 和 按钮 遵循命名标准 例如 Value1Tb Value1Cmd 和 Value2Tb Value2Cmd 我的问题是 因为我想对每个按钮执行相同的操作
  • 哪个 Eclipse 可以与 ADT 完美配合?

    Eclipse 有很多版本 例如 靛蓝 朱诺 开普勒 月球 火星 其中哪一个最适合 ADT Stack Overflow 上有很多这样的问题 但都是 4 5 年前的问题 我正在寻找更新的东西 我提出你的问题是因为我自己也想知道这个问题 因为
  • 当一个子类没有额外属性时,教义表类继承

    我的映射有问题 我无法让它工作 我有一个像这样的抽象基类 Entity Table name actions InheritanceType JOINED DiscriminatorColumn name type type string
  • 依赖注入和开发效率

    Abstract 在过去的几个月里 我一直在编写一个轻量级 基于 C 的游戏引擎 具有 API 抽象和实体 组件 脚本系统 它的整体理念是通过提供类似于 Unity 引擎的架构来简化 XNA SlimDX 等游戏开发过程 设计挑战 正如大多