我们如何确定何时使用依赖注入或单例模式。
我在很多网站上读到过他们说“使用依赖注入而不是单例模式”。但我不确定是否完全同意他们的观点。对于我的小型或中型项目,我肯定看到单例模式的用途很直接。
例如Logger。我可以使用Logger.GetInstance().Log(...)
。但是,我为什么需要将我创建的每个类都注入日志记录器的实例呢?
我们如何确定何时使用依赖注入或单例模式。
我在很多网站上读到过他们说“使用依赖注入而不是单例模式”。但我不确定是否完全同意他们的观点。对于我的小型或中型项目,我肯定看到单例模式的用途很直接。
例如Logger。我可以使用Logger.GetInstance().Log(...)
。但是,我为什么需要将我创建的每个类都注入日志记录器的实例呢?
单例模式就像共产主义一样:在理论上听起来不错,但实践中却会遇到很多问题。
单例模式过分强调对象访问的便捷性。它完全忽略了上下文,要求每个使用者都使用一个应用程序域范围内的对象,没有为不同的实现留下选项。它将基础知识嵌入到您的类中(调用GetInstance()
),同时添加了零表现力。它实际上降低了您的表现力,因为您无法更改一个类使用的实现,而不更改所有类的实现。您不能添加一次性功能。
此外,当类 Foo
依赖于 Logger.GetInstance()
时, Foo
有效地隐藏了其依赖关系,这意味着您不能完全理解 Foo
或自信地使用它,除非您阅读其源代码并发现其依赖于 Logger
。如果您没有源代码,那么这将限制您了解和有效使用所依赖的代码的能力。
作为使用静态属性/方法实现的单例模式仅仅是围绕实施基础设施的一种hack。它以无数种方式限制了您,而在可替代方案中没有任何明显的好处。虽然您可以随意使用它,但是由于存在促进更好设计的可行替代方案,因此它永远不应成为推荐实践。
Singleton.getInstance()
方法获取。其他人已经很好地解释了单例模式的问题。我只想补充一下关于Logger特定情况的说明。我同意你通常可以通过静态getInstance()
或getRootLogger()
方法作为单例来访问Logger(或根记录器)。(除非你想查看测试所记录的内容 - 但根据我的经验,我几乎没有遇到过这种必须要求的情况。当然,对于其他人来说可能更紧迫)。
在我看来,通常单例日志记录器并不是一个问题,因为它不包含与您正在测试的类相关的状态。也就是说,日志记录器的状态(及其可能的更改)对被测试类的状态没有任何影响。因此,它不会使您的单元测试变得更加困难。
另一种选择是通过构造函数将日志记录器注入到几乎每个应用程序中的所有类中。为了接口的一致性,即使涉及的类目前不记录任何内容,也应该进行注入。否则,当您在某些时候发现需要从此类记录日志时,您需要一个日志记录器,因此需要添加一个构造函数参数以实现依赖注入,破坏所有客户端代码的稳定性。我不喜欢这两个选项,我认为使用DI进行日志记录只是为了遵循一个理论规则而使我的生活变得更加复杂,没有任何实际的好处。
因此,我的结论是:如果一个类被(几乎)普遍使用但不包含与您的应用程序相关的状态,则可以安全地将其实现为单例。
这主要与测试有关,但不完全只是如此。单例模式之所以流行,是因为它们易于使用,但是单例模式有许多缺点。
DI(依赖注入)可以让您轻松地使用依赖类 - 只需将其放在构造函数参数中,系统会自动提供 - 同时为您提供测试和构建灵活性。
关于使用Singleton而不是依赖注入的唯一情况是,如果Singleton表示一个不可变值,例如List.Empty或类似的内容(假设是不可变列表)。
对于Singleton的核心问题应该是“如果这是一个全局变量而不是Singleton,我是否可以接受?” 如果不能,那么您正在使用Singleton模式来掩盖全局变量,并且应该考虑其他方法。
刚刚看了一下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,则可以跳过获取其依赖项的步骤。 虽然很整洁,但也有点凌乱,对吧?
单例模式有其他替代方案:代理模式和MonoState模式。
http://www.objectmentor.com/resources/articles/SingletonAndMonostate.pdf