65.9K
CodeProject 正在变化。 阅读更多。
Home

单例模式 – 积极和消极方面

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (36投票s)

2011年12月28日

CPOL

5分钟阅读

viewsIcon

183936

本文介绍了单例模式的优点和缺点。

引言

单例模式可能是我们所知的最著名,同时也是最具争议的模式。它也必定是最容易学习和实现的模式。与任何其他模式一样,单例模式是为了解决一个常见的业务问题——“管理资源的状态”。但它解决了真正的问题还是引入了额外的问题?这正是我在此涵盖的主题。

单例的优点

最难调试的问题之一是由管理单个资源状态的类的多个实例所创建。如果我们能使用某种设计模式来控制对该共享资源的访问,那将是高度期望的。单例模式非常适合解决这种情况;通过将一个单例类封装在这个问题周围,可以确保在任何给定时间只有一个类的实例。用于日志记录的类是单例类的最常见和陈词滥调的例子,因为整个应用程序随时只需要一个日志记录器实例。

单例类的结构非常容易理解。该类通常有一个private构造函数,该构造函数会禁止你创建单例类的任何实例;相反,你将访问单例类的static属性或static函数来获取预配置实例的引用。这些属性/方法确保在应用程序的整个生命周期内只有一个单例类的实例。

单例类的唯一实例是在单例类内部创建的,并且其引用由调用者使用。可以使用以下任何一种方法来创建实例:

1. 延迟实例化

如果您选择延迟实例化范例,则在首次调用用于返回引用的属性或函数之前,单例变量将不会获得内存。如果您设计的单例类占用大量资源,这种类型的实例化将非常有用。

然而,上述实现没有采取任何措施来确保线程安全。也就是说,可能出现多个线程同时访问Instance属性的情况,这会导致创建多个单例类的实例。

我们可以使用各种线程同步技术来应对上述情况。一种方法是使用双重检查锁定。在双重检查锁定中,同步仅在单例变量为null时生效,即仅在首次调用Instance时生效。这有助于我们将同步对象带来的性能损失限制在只发生一次。

2. 静态初始化

在静态初始化中,在声明变量时为其分配内存。当首次访问任何成员单例类时,实例创建会在后台进行。这种类型实现的主要优点是 CLR 会自动处理我在延迟实例化中解释过的竞态条件。我们无需在此处使用任何特殊的同步构造。当您从延迟实例化切换到静态初始化时,单例实现的重大代码更改不多。唯一的改变是将对象创建部分移动到我们声明变量的位置。

单例类的继承

应该禁止继承单例类。使单例类可继承意味着任何数量的子类都可以继承它,从而创建多个单例类的实例,这显然会违反单例原则。

单例类与静态方法

单例类在以下方面优于static类:

  1. Static类不促进继承。如果您的类需要派生自某个接口,static类将使其不可能。
  2. 您无法使用static方法指定任何创建逻辑。
  3. Static方法是过程式代码。

单例的缺点

以下几点被用来反对单例模式:

  1. 它们偏离了单一职责原则。单例类除了其他业务职责外,还有创建自身实例的职责。然而,可以通过将创建部分委托给工厂对象来解决此问题。
  2. 单例类不能被子类化。
  3. 单例会隐藏依赖项。高效系统架构的一个特点是最小化类之间的依赖关系。这反过来有助于您在进行单元测试和将程序的任何部分隔离到单独的程序集中。单例会让你牺牲应用程序中的这一特性。由于对象创建部分对我们是不可见的,我们不能期望单例构造函数接受任何参数。这个缺点乍看之下似乎并不重要,但随着软件复杂性的增加,它会限制程序的灵活性。

程序员经常求助于依赖注入的想法来克服这个问题。当使用依赖注入时,单例实例不会在类内部检索,而是通过构造函数或属性传递。

IMathFace obj = Singleton.Instance;
SingletonConsumer singConsumer = new SingletonConsumer(obj);
singConsumer.ConsumerAdd(10,20);

在上面的示例中,SingletonConsumer的构造函数接受类型为IMathFace的参数。它可以是真实的单例实例,也可以是模拟对象。

何时使用单例类?

这个问题没有直接的答案。对某些人来说可接受的情况对另一些人来说是不可接受的。

然而,普遍认为,在应用程序的各个部分同时尝试访问共享资源的情况下,单例可以产生最佳结果。共享资源的示例包括Logger、打印后台程序等。

以下几点被建议在设计单例类时予以考虑:

  1. 单例类必须没有内存泄漏。单例类的实例只创建一次,并且在应用程序的整个生命周期内都存在。
  2. 真正的单例类不易扩展。
  3. 从接口派生单例类。这有助于进行单元测试(使用依赖注入)。

结论

单例模式取代全局变量的概念并不总是正确的。在决定使用单例模式之前,您应该牢记其所有优点和缺点。

© . All rights reserved.