1.14 单例模式
单例模式(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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论