依赖注入; 减少样板代码的良好做法


25

我有一个简单的问题,我甚至不确定它是否有答案,但让我们尝试。我使用C ++进行编码,并使用依赖注入来避免全局状态。这工作得很好,而且我不会经常出现意外/未定义的行为。

但是我意识到,随着项目的发展,我正在编写许多我认为很简单的代码。更糟糕的是:事实上,比起实际的代码,样板代码更多,因此有时很难理解。

没有什么比一个好的例子更好了,让我们开始吧:

我有一个名为TimeFactory的类,它创建Time对象。

有关更多详细信息(不确定是否相关):时间对象非常复杂,因为时间可以具有不同的格式,并且它们之间的转换既不是线性的也不是简单的。每个“时间”都包含一个同步器来处理转换,并确保它们具有相同的,正确初始化的同步器,我使用TimeFactory。TimeFactory只有一个实例,并且在应用程序范围内,因此它可以容纳单例,但是由于它是可变的,因此我不想将其设置为单例。

在我的应用中,很多类都需要创建Time对象。有时,这些类是深层嵌套的。

假设我有一个类A,其中包含类B的实例,依此类推,直到类D。类D需要创建Time对象。

在我的幼稚实现中,我将TimeFactory传递给类A的构造函数,然后将其传递给类B的构造函数,依此类推,直到类D。

现在,假设我有几个类(如TimeFactory)和几个类层次结构(如上一个类):我失去了使用依赖注入获得的所有灵活性和可读性。

我开始怀疑我的应用程序中是否没有主要的设计缺陷……或者这是使用依赖注入的必要弊端?

你怎么看 ?


5
我向程序员而不是stackoverflow发布了这个问题,因为据我所知,程序员更多地用于与设计相关的问题,而stackoverflow则用于“当您在编译器面前时”的问题。抱歉,如果我错了。
Dinaiz 2012年

2
面向方面的编程,也很适合执行此任务-zh.wikipedia.org/wiki/Aspect-oriented_programming
EL Yusubov 2012年

是B,C和D类的A类工厂吗?如果不是,为什么要创建它们的实例?如果只有D类需要时间对象,为什么还要用TimeFactory注入其他任何类?D类是否真的需要TimeFactory,还是可以将Time的一个实例注入其中?
simoraman 2012年

D需要创建Time对象,只包含D应该具有的信息。我必须考虑其余的意见。我的代码中“谁在创造谁”可能存在问题
Dinaiz 2012年

IoC容器解决了“谁在创建谁”的问题,有关详细信息,请参见我的答案。“谁在创建谁”是使用依赖注入时可以问的最好的问题之一。
GameDeveloper

Answers:


23

在我的应用中,很多类都需要创建Time对象

似乎您的Time类是一种非常基本的数据类型,应属于您应用程序的“通用基础结构”。DI不适用于此类课程。考虑一下,如果必须将类之类string注入使用字符串的代码的每个部分,这意味着什么,而您将需要使用a stringFactory作为创建新字符串的唯一可能性-程序的可读性将降低大约大小。

因此,我的建议是:不要将DI用于一般数据类型,例如Time。为Time类本身编写单元测试,完成后就可以在程序中的任何地方使用它,就像string该类,vector标准库的一个类或任何其他类一样。将DI用于应该真正彼此分离的组件。


您完全正确。“程序的可读性将降低一个数量级。” :确实是这样。如果我仍然想使用工厂,那么使它成为单身人士还不错吗?
Dinaiz '08

3
@Dinaiz:想法是接受Time程序与程序其他部分之间的紧密耦合。因此,您可以接受也接受与的紧密耦合TimeFactory。但是,我要避免的是只有一个TimeFactory带有状态(例如,区域信息或类似信息)的全局对象-这可能会对程序造成不良影响,并使常规测试和重用变得非常困难。使其成为无状态,或者不将其用作单例。
布朗

实际上,它必须具有状态,因此当您说“不要将其用作单例”时,您的意思是“继续使用依赖注入,即将实例传递给需要它的每个对象”,对吗?
Dinaiz

1
@Dinaiz:说实话,这取决于状态,使用Time和的程序部分的种类和数量,与TimeFactory将来有关的扩展所需的“可扩展性”程度TimeFactory,等等。
布朗

好吧,我会尽量保持依赖注入,但是有一个更聪明的设计,那就不需要那么多的样板了!非常感谢文档”
Dinaiz 2012年

4

您的意思是“我放弃了使用依赖注入获得的所有灵活性和可读性”-DI与可读性无关。这是关于分离对象之间的依赖关系。

听起来您有A类创建B类,B类创建C类和C类创建D类。

您应该拥有的是将B级注入到A级,将C级注入到B级,将D级注入到C级。


DI可能与可读性有关。如果您在方法中间出现了一个“神奇地”出现的全局变量,那么从维护的角度来看,这无助于理解代码。这个变量来自哪里?谁拥有它?谁初始化它?依此类推...对于您的第二点,您可能是对的,但我认为这不适用于我的情况。感谢您抽出
宝贵

DI增加了可读性,主要是因为“新/删除”将被神奇地从您的代码中删除。如果您不必担心动态分配代码,则更易于阅读。
GameDeveloper 2013年

还请忘记说这会使代码更强大,因为无法再假设“只是”需要依赖项的“如何”创建。这使类的维护更加容易..参见我的回答
GameDeveloper 2013年

3

我不确定您为什么不想让自己的时间工厂变成单身人士。如果整个应用程序中只有一个实例,则实际上是一个实例。

话虽如此,共享一个可变对象是非常危险的,除非它由同步块适当地保护,在这种情况下,没有理由不让它成为单例对象。

如果要进行依赖项注入,则可能需要查看spring或其他依赖项注入框架,这将允许您使用注释自动分配参数。


Spring适用于Java,我使用C ++。但是为什么有2个人对您投反对票?您的帖子中有些观点我不同意但仍然...
Dinaiz 2012年

我将假定不赞成投票的原因是不回答问题本身,也许还有提到Spring。但是,在我看来,使用Singleton的建议是一个有效的建议,可能无法回答问题-而是提出了有效的替代方案,并提出了一个有趣的设计问题。因此,我已+1。
弗格斯(Fergus)在伦敦,2013年

1

这是对Doc Brown的补充回答,也是对Dinaiz仍与该问题相关的未回答评论的回应。

您可能需要的是进行DI的框架。具有复杂的层次结构并不一定意味着设计不好,但是如果您必须自下而上地注入TimeFactory(从A到D),而不是直接注入D,那么您进行依赖注入的方式可能有问题。

一个人吗?不用了,谢谢。如果您只需要一个等距使其在整个应用程序上下文中共享(使用像Infector ++这样的DI的IoC容器只需要将TimeFactory绑定为单个等距),下面就是示例(顺便说一下C ++ 11,但是C ++。到C ++ 11了吗?您可以免费获得Leak-Free应用程序):

Infector::Container ioc; //your app's context

ioc.bindSingleAsNothing<TimeFactory>(); //declare TimeFactory to be shared
ioc.wire<TimeFactory>(); //wire its constructor 

// if you want to be sure TimeFactory is created at startup just request it
// (else it will be created lazily only when needed)
auto myTimeFactory = ioc.buildSingle<TimeFactory>();

现在,IoC容器的好处是您不需要将时间工厂传递给D。如果您的类“ D”需要时间工厂,只需将时间工厂作为类D的构造函数参数即可。

ioc.bindAsNothing<A>(); //declare class A
ioc.bindAsNothing<B>(); //declare class B
ioc.bindAsNothing<D>(); //declare class D

//constructors setup
ioc.wire<D, TimeFactory>(); //time factory injected to class D
ioc.wire<B, D>(); //class D injected to class B
ioc.wire<A, B>(); //class B injected to class A

如您所见,您只注入了一次TimeFactory。如何使用“ A”?非常简单,每个类都可以注入,建立在主体中或由工厂负责。

auto myA1 = ioc.build<A>(); //A is not "single" so many different istances
auto myA2 = ioc.build<A>(); //can live at same time

每次创建类A时,它将自动(懒惰)注入所有依赖项,直到D,并且D将注入TimeFactory,因此,仅调用1个方法就可以准备完整的层次结构(甚至可以通过这种方式解决复杂的层次结构删除很多样板代码):您不必调用“ new / delete”,这非常重要,因为您可以将应用程序逻辑与粘合代码分开。

D可以使用仅D可以拥有的信息来创建Time对象

这很容易,您的TimeFactory有一个“创建”方法,然后只需使用其他签名“创建(参数)”就可以完成。不依赖项的参数通常以这种方式解析。这也消除了注入诸如“弦”或“整弦”之类的东西的责任,因为这只会增加额外的锅炉板。

谁创造谁?IoC容器创建等距物和工厂,工厂创建其余物(工厂可以创建具有任意参数的不同对象,因此您实际上不需要工厂的状态)。您仍然可以将工厂用作IoC容器的包装器:一般而言,在IoC容器中注入Injectin非常糟糕,与使用服务定位器相同。某些人通过用工厂包装IoC容器解决了这个问题(这不是严格必要的,但优点是容器可以解决层次结构,并且您所有的工厂都变得更加易于维护)。

//factory method
std::unique_ptr<myType> create(params){
    auto istance = ioc->build<myType>(); //this code's agnostic to "myType" hierarchy
    istance->setParams(params); //the customization you needed
    return std::move(istance); 
}

同样不要滥用依赖注入,简单类型可以只是类的成员或局部范围的变量。这似乎很明显,但是我看到人们注入“ std :: vector”只是因为有一个允许这样做的DI框架。永远记住Demeter的定律:“只注入您真正需要注入的东西”


哇,我之前没看到您的回答,对不起!先生,内容非常丰富!Upvoted
Dinaiz '16

0

您的A,B和C类还需要创建Time实例还是仅创建D类?如果只有D类,那么A和B应该对TimeFactory一无所知。在C类中创建TimeFactory的实例,并将其传递给D类。请注意,通过“创建实例”,我并不一定意味着C类必须负责实例化TimeFactory。它可以从类B接收DClassFactory,并且DClassFactory知道如何创建Time实例。

当我没有任何DI框架时,我也经常使用的一种技术是提供两个构造函数,一个构造函数接受一个工厂,另一个构造一个创建默认工厂。第二个通常具有受保护/程序包访问权限,并且主要用于单元测试。


-1

我实现了另一个C ++依赖注入框架,该框架最近被提出来进行增强– https://github.com/krzysztof-jusiak/di –库少了宏(免费),仅标头,C ++ 03 / C ++ 11 / C ++ 14库提供类型安全,编译时,无宏的构造函数依赖项注入。


3
在P.SE上回答旧问题是宣传您的项目的一种糟糕方法。考虑到那里有成千上万的项目可能与某些问题相关。如果他们的所有作者都在此处发布了此类内容,则该网站的回答质量将直线下降。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.