返回介绍

1.14 单例模式

发布于 2025-01-04 00:44:55 字数 8036 浏览 0 评论 0 收藏 0

单例模式(Singleton Pattern) 可以确保一个具体的类只创建过一个对象。它非常适用于系统中只需要一个对象来协调行为:一方面是处于在有些情况下,创建多个等同的

单例模式的缺点包括以下两方面:

  • 减少重用。比如,如果想利用单例模式来继承,就会出现问题。如果 SingletonB 扩展了 SingletonA ,那么就会(最多)只有一个实例。如果你想让两个类都有一个实例,那么如何重写 getInstance() 静态方法呢?
  • 由于静态方法,通常也很难测试单例,但如果需要时,Groovy 也能支持这一点。

1.14.1 经典 Java 单例

假设要设计一个收集选票的类,由于选票数量是非常重要的因素,所以决定使用单例模式。 VoteCollector 对象只能出现一个,所以很容易创建并使用对象。

class VoteCollector {
  def votes = 0
  private static final INSTANCE = new VoteCollector()
  static getInstance() { return INSTANCE }
  private VoteCollector() { }
  def display() { println "Collector:${hashCode()}, Votes:$votes" }
}

代码中值得注意的是:

  • 包含一个私有的构造函数,因而不可能在系统中创建 VoteCollector 对象(除了我们创建的 INSTANCE )。
  • INSTANCE 也是私有的,所以一旦设置好了,就无法更改它。
  • 暂时,选票更新还无法做到线程安全(该范例没有提供这个功能)。
  • 选票收集器实例的创建并不是延后创建的(如果我们永远不引用该类,就不会创建该实例;然而,一旦引用该类,就会创建该实例,即使一开始并不需要)。

可以在一些脚本代码中使用该单例类:

def collector = VoteCollector.instance
collector.display()
collector.votes++
collector = null

Thread.start{
  def collector2 = VoteCollector.instance
  collector2.display()
  collector2.votes++
  collector2 = null
}.join()

def collector3 = VoteCollector.instance
collector3.display()

实例被使用了 3 次,第 2 次使用甚至是处于不同的线程(但不要在存在一个新的类加载器的情况下这样做)。

运行该脚本会得到以下输出(hashcode 值会变更):

Collector:15959960, Votes:0
Collector:15959960, Votes:1
Collector:15959960, Votes:2

该模式的变体形式为:

  • 为了支持延后加载及多线程,可以只使用 synchronized 关键字和 getInstance() 方法。虽然在性能上可能会出现问题,但却奏效。
  • 包含双重检查锁定模式和 volatile 关键字(对于 Java 5 及以前版本),但是这种做法存在以下局限性: 局限性

1.14.2 范例:通过元编程实现单例模式

利用 Groovy 元编程功能可以让类似单例模式这样的概念得到更为根本的展现。下面的范例展示了如何使用 Groovy 的元编程功能实现单例模式,但不一定是最高效的方式。

假设要跟踪计算器执行的计算总数,一种方法是为计算器类使用单例,在类中设置一个保存计数的变量。

首先定义一些基础类。 Calculator 类用于执行计算,并记录执行了多少次计算。 Client 类是计算器的外在接口。

class Calculator {
  private total = 0
  def add(a, b) { total++; a + b }
  def getTotalCalculations() { 'Total Calculations: ' + total }
  String toString() { 'Calc: ' + hashCode() }
}

class Client {
  def calc = new Calculator()
  def executeCalc(a, b) { calc.add(a, b) }
  String toString() { 'Client: ' + hashCode() }
}

接着,定义并注册一个 超级类MetaClass),阻止再创建 Calculator 对象,并提供一个预先创建好的实例。也要在 Groovy 系统中注册这个超级类。

class CalculatorMetaClass extends MetaClassImpl {
  private static final INSTANCE = new Calculator()
  CalculatorMetaClass() { super(Calculator) }
  def invokeConstructor(Object[] arguments) { return INSTANCE }
}

def registry = GroovySystem.metaClassRegistry
registry.setMetaClass(Calculator, new CalculatorMetaClass())

现在,就可以从脚本中使用 Client 类的实例。 Client 类试图创建新的计算器实例,但却得到的是单例。

def client = new Client()
assert 3 == client.executeCalc(1, 2)
println "$client, $client.calc, $client.calc.totalCalculations"

client = new Client()
assert 4 == client.executeCalc(2, 2)
println "$client, $client.calc, $client.calc.totalCalculations"

运行该脚本的结果如下(hashcode 值可能不同):

Client: 7306473, Calc: 24230857, Total Calculations: 1
Client: 31436753, Calc: 24230857, Total Calculations: 2  

1.14.3 Guice 范例

也可以使用 Guice 来实现单例模式。

还是以计算器为例。

Guice 是一种面向 Java 的框架,支持面向接口的设计,因而首先创建 Calculator 接口。然后创建 CalculatorImpl 实现,以及一个用来与脚本交互的 Client 对象。严格意义上,本例并不需要 Client 类,但它却能让我们展示非单例实例是默认的。代码如下:

@Grapes([@Grab('aopalliance:aopalliance:1.0'), @Grab('com.google.code.guice:guice:1.0')])
import com.google.inject.*

interface Calculator {
  def add(a, b)
}

class CalculatorImpl implements Calculator {
  private total = 0
  def add(a, b) { total++; a + b }
  def getTotalCalculations() { 'Total Calculations: ' + total }
  String toString() { 'Calc: ' + hashCode() }
}

class Client {
  @Inject Calculator calc
  def executeCalc(a, b) { calc.add(a, b) }
  String toString() { 'Client: ' + hashCode() }
}

def injector = Guice.createInjector (
  [configure: { binding ->
    binding.bind(Calculator)
         .to(CalculatorImpl)
         .asEagerSingleton() } ] as Module
)

def client = injector.getInstance(Client)
assert 3 == client.executeCalc(1, 2)
println "$client, $client.calc, $client.calc.totalCalculations"

client = injector.getInstance(Client)
assert 4 == client.executeCalc(2, 2)
println "$client, $client.calc, $client.calc.totalCalculations"

注意 Client 类中的 @Inject 注释。可以在代码中正确地指明注入的字段。

在该例中,我们选择使用显式的绑定。所有的依赖关系(目前只有一个)都配置在绑定中。Guice 注入器了解绑定并在创建对象时注入所需的依赖。为了维持单例模式,必须使用 Guice 来创建实例。至今为止,你还可以手动地使用 CalculatorImpl() 创建计算器实例,这显然会侵犯预定的单例行为。

在其他的一些情况下(可能在大型系统中),我们可以用注释来表达依赖,如下所示:

@Grapes([@Grab('aopalliance:aopalliance:1.0'), @Grab('com.google.code.guice:guice:1.0')])
import com.google.inject.*

@ImplementedBy(CalculatorImpl)
interface Calculator {
  // as before ...
}

@Singleton
class CalculatorImpl implements Calculator {
  // as before ...
}

class Client {
  // as before ...
}

def injector = Guice.createInjector()

// ...

注意 CalculatorImpl 类中的 @Singleton 注释,以及 Calculator 接口中的 @ImplementedBy 注释。

使用任何一种方法运行上述范例后的结果为(hashcode 值可能会不同):

Client: 8897128, Calc: 17431955, Total Calculations: 1  
Client: 21145613, Calc: 17431955, Total Calculations: 2  

无论在什么时候,只要请求实例,就会获得了一个新的客户端对象,但它却被同样一个计算器对象所注入。

1.14.4 Spring 范例

使用 Spring 再来实现一下计算器范例:

@Grapes([@Grab('org.springframework:spring-core:3.2.2.RELEASE'), @Grab('org.springframework:spring-beans:3.2.2.RELEASE')])
import org.springframework.beans.factory.support.*

interface Calculator {
  def add(a, b)
}

class CalculatorImpl implements Calculator {
  private total = 0
  def add(a, b) { total++; a + b }
  def getTotalCalculations() { 'Total Calculations: ' + total }
  String toString() { 'Calc: ' + hashCode() }
}

class Client {
  Client(Calculator calc) { this.calc = calc }
  def calc
  def executeCalc(a, b) { calc.add(a, b) }
  String toString() { 'Client: ' + hashCode() }
}

// 通过 API 来连接依赖。
// 还可以使用基于 XML 的配置,或 Grails Bean Builder DSL。   
def factory = new DefaultListableBeanFactory()
factory.registerBeanDefinition('calc', new RootBeanDefinition(CalculatorImpl))
def beanDef = new RootBeanDefinition(Client, false)
beanDef.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_AUTODETECT)
factory.registerBeanDefinition('client', beanDef)

def client = factory.getBean('client')
assert 3 == client.executeCalc(1, 2)
println "$client, $client.calc, $client.calc.totalCalculations"

client = factory.getBean('client')
assert 4 == client.executeCalc(2, 2)
println "$client, $client.calc, $client.calc.totalCalculations"  

运行结果如下(hashcode 值可能不同):

Client: 29418586, Calc: 10580099, Total Calculations: 1
Client: 14800362, Calc: 10580099, Total Calculations: 2  

1.14.5 更多资源

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文