依赖注入和单例设计模式


88

我们如何确定何时使用依赖注入或单例模式。我在许多网站上都读过,他们说“对单例模式使用依赖注入”。但是我不确定我是否完全同意他们的观点。对于我的中小型项目,我肯定看到单例模式的使用很简单。

例如记录器。我可以使用Logger.GetInstance().Log(...) But,而不是使用它,为什么我需要使用记录器的实例注入我创建的每个类?

Answers:


65

如果要验证测试中记录的内容,则需要进行依赖注入。此外,记录器很少是单例的-通常每个班级都有一个记录器。

观看有关面向对象设计的可演示性的演示,您将了解为什么单例不好。

单例的问题在于它们代表了难以预测的全局状态,尤其是在测试中。

请记住,对象可以是事实上的单例对象,但仍可以通过依赖注入而不是通过获得Singleton.getInstance()

我只是在Misko Hevery的演讲中列出了一些重要观点。看着它后,你将获得关于为什么它是最好有一个对象定义全透视什么的依赖关系,但不能定义一个方法如何创建它们。


89

单身人士就像共产主义:在纸面上听起来都很棒,但在实践中却遇到了问题。

单例模式过分强调访问对象的难易程度。它要求每个使用者都使用AppDomain范围的对象,从而完全避开了上下文,而对于各种实现都没有选择余地。它将基础设施知识嵌入到您的类中(对的调用GetInstance()),同时添加了完全为零的表达能力。实际上,它降低了您的表达能力,因为您不能更改一个类使用的实现,而不必为所有这些类更改。您根本无法添加一次性功能。

同样,当classFoo依赖于时Logger.GetInstance()Foo可以有效地将其依赖项对消费者隐藏。这意味着您不能完全理解Foo或使用它,除非您阅读它的来源并发现它依赖的事实Logger。如果您没有源代码,那将限制您理解和有效使用所依赖代码的能力。

用静态属性/方法实现的单例模式仅是围绕实现基础结构的技巧。它以多种方式限制了您,同时没有任何其他替代品的明显好处。您可以根据需要使用它,但是由于有可行的替代方法可以促进更好的设计,因此永远不建议这样做。


9
@BryanWatts表示,在普通的中型应用程序中,Singletons仍然更快,更不容易出错。通常,(自定义)记录器没有多个可能的实现,所以为什么我需要**:1.为此创建一个接口,并在每次我需要添加/更改公共成员时更改它。2.维护DI配置3.掩盖整个系统中只有一个这种类型的对象的事实。4.将自己绑定到由过早分离关注点引起的严格功能。
Uri Abramson 2014年

@UriAbramson:看来您已经就自己喜欢的取舍做出了决定,所以我不会说服您。
布莱恩·瓦茨

@BryanWatts情况并非如此,也许我的评论有点过于刺耳,但实际上我确实非常感兴趣,因为我听到您对我提出的观点有何评论……
Uri Abramson 2014年

1
@UriAbramson:足够公平。您是否至少同意在测试过程中将实现交换为隔离很重要?
布莱恩·瓦茨

5
单例的使用和依赖项注入不是互斥的。单例可以实现接口,因此可以用来满足对另一个类的依赖。它是单例的事实并不能迫使每个使用者都通过其“ GetInstance”方法/属性获得引用。
奥利弗

18

其他人通常已经很好地解释了单例的问题。我只想添加有关Logger特定情况的注释。我同意您的看法,通过静态getInstance()getRootLogger()方法以单例方式访问Logger(准确地说是根记录器)通常不是问题。(除非您想查看正在测试的班级记录了什么,但是以我的经验,我很难回想起这种情况是必须的。对于其他人,这可能是一个更紧迫的问题)。

IMO通常不担心单例记录器,因为它不包含与您正在测试的类相关的任何状态。也就是说,记录器的状态(及其可能的更改)对测试类的状态没有任何影响。因此,它不会使您的单元测试变得更加困难。

替代方法是通过构造函数注入记录器,以(几乎)应用程序中的每个类。为了确保接口的一致性,即使所讨论的类当前未记录任何内容,也应注入该接口-替代方法是,当您发现某个时刻现在需要从该类中记录某些内容时,需要一个记录器,因此您需要为DI添加构造函数参数,从而破坏所有客户端代码。我不喜欢这两个选项,并且我觉得使用DI进行日志记录只会使我的生活变得复杂,以遵守理论规则,而没有任何具体的好处。

因此,我的底线是:可以(几乎)普遍使用但不包含与您的应用相关的状态的类,可以安全地实现为Singleton


1
即使那样,单身也可能会很痛苦。如果您意识到记录器在某些情况下需要一些其他参数,则可以采用一种新方法(使记录器变得更丑陋),或者破坏记录器的所有使用方,即使他们不在乎。
kyoryu

4
@kyoryu,我正在谈论“常规”情况,恕我直言暗示使用(事实上的)标准日志记录框架。(通常可以通过属性/ XML文件进行配置。)当然也有例外-一如既往。如果我知道我的应用在这方面表现出色,那我就不会使用Singleton。但是,过度设计说“有时可能有用”几乎总是浪费精力。
彼得Török

如果您已经在使用DI,则不需要太多的工程设计。(顺便说一句,我不同意你的看法,我是支持者)。许多记录器需要一些“类别”信息或类似的信息,并且添加其他参数可能会导致痛苦。将其隐藏在接口后面可以帮助保持使用中的代码干净,并可以轻松切换到其他日志记录框架。
kyoryu 2010年

1
@kyoryu,对不起错误的假设。我看到了一张反对票和一条评论,于是我把点连接了起来-事实证明是错误的方式:-(我从未经历过切换到其他日志记录框架的需要,但是再次,我明白这可能是合法的关注在一些项目。
彼得Török

5
如果我错了,请纠正我,但是单例将用于LogFactory,而不是记录器。另外,LogFactory可能是apache commons或Slf4j日志外观。因此,无论如何,切换日志记录实现都是轻松的。使用DI注入LogFactory的真正痛苦不是您现在必须进入applicationContext才能在应用程序中创建每个实例的事实吗?
HDave 2010年

9

主要是测试,但并不是全部。Singlton之所以受欢迎是因为它很容易食用,但是单身人士有很多缺点。

  • 很难测试。这意味着我如何确保记录器执行正确的操作。
  • 很难测试。这意味着,如果我正在测试使用记录器的代码,但这不是测试的重点,那么我仍然需要确保我的测试环境支持记录器
  • 有时您不想要辛格尔顿,但更灵活

DI使您可以轻松使用依赖类-只需将其放在构造函数args中,系统就会为您提供它-同时为您提供测试和构造灵活性。


并不是的。从长远来看,这与共享的可变状态和静态依赖关系有关。测试只是显而易见的例子,并且通常是最痛苦的例子。
kyoryu 2010年

2

如果Singleton表示不可变值,例如List.Empty等(假设不可变列表),则大约只有一次应该使用Singleton而不是Dependency Injection。

单例的直觉检查应该是“如果这是全局变量而不是单例,我可以吗?” 如果不是,则您使用的是Singleton模式来覆盖全局变量,因此应考虑使用其他方法。


1

刚刚查看了Monostate的文章-它是Singleton的不错选择,但它具有一些奇怪的属性:

class Mono{
    public static $db;
    public function setDb($db){
       self::$db = $db;
    }

}

class Mapper extends Mono{
    //mapping procedure
    return $Entity;

    public function save($Entity);//requires database connection to be set
}

class Entity{
public function save(){
    $Mapper = new Mapper();
    $Mapper->save($this);//has same static reference to database class     
}

$Mapper = new Mapper();
$Mapper->setDb($db);

$User = $Mapper->find(1);
$User->save();

并不是很可怕-因为Mapper实际上依赖于数据库连接来执行save()-但是如果先前已经创建了另一个Mapper-它可以跳过此步骤来获取其依赖项。虽然很整洁,但也很杂乱,不是吗?


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.