单例模式是一种常用的设计模式,在ruby的标准库中有singleton模块,可以直接像下面这样使用:
require 'Singleton'
class Test
include Singleton
end
a, b = Test.instance, Test.instance
puts a==b #=>true
如果不使用标准库,该如何实现呢?在看完《ruby元编程》第五章后,似乎有了一些思路决定自己实现一个(备注:和标准库的实现方式可能不一致,还没有看过标准库)。首先需要一些准备知识:钩子函数Hook Method(回调函数),Ruby在做一些事情的时候,会回调一些函数。如继承。例子代码如下:
class String
def self.inherited(subclass)
puts "#{self} was inherited by #{subclass}"
end
end
class T < String; end
类在继承时会调用Class#inherited方法。所以上述代码会输出:
String was inherited by T
还有其它很多的钩子函数,这里用到的一个就是included方法,当模块被包含的时候,会调用该方法。例子代码如下:
module M
def self.included(base)
puts "#{self} included by #{base}"
end
end
class Test
include M
end
# => M included by Test
有了以上的工具就可以实现Singleton了。如下代码实现了instance函数:
module Singleton
def self.included(base)
base_eigenclass = class << base; self; end
base_eigenclass.class_eval do
instance_variable_set("@instance", base.new)
define_method :instance do
instance_variable_get "@instance"
end
end
end
end
使用如下代码测试:
# test for singleton
class Test
include Singleton
end
a, b = Test.instance, Test.instance
puts a==b #=>true
单例模式为了保证只有一个方式来得到类实例,不能使用new函数来生成实例,在类定义中,我们可以使用private:new 来讲new方法设置为私有,这样就阻止了对new方法的调用。但是在这里如何阻止呢?我们知道new方法是Class类的实例方法,那么通过覆写该方法,使其抛出一个异常,就可以达到目的了。重新修改后,代码如下:
module Singleton
def self.included(base)
base_eigenclass = class << base; self; end
base_eigenclass.class_eval do
instance_variable_set("@instance", base.new)
define_method :instance do
instance_variable_get "@instance"
end
define_method :new do
raise "can't new in singleton class"
end
end
end
end
通过调用test.new可以看到抛出异常,这样就实现了一个简单的singleton模块。以后通过如下方法即可使用了:
class Test
include Singleton
end
a = Test.instance
观察以上代码,通过使用Eigenclass来定义base的类方法,有点不够简洁,class中可以通过extend方法直接将Module中的方法加载到Eigenclass,那么何不在Module中直接使用extend呢,修改后,第二个版本的Singleton Module代码如下:
module Singleton
def self.included(base)
base.instance_variable_set("@instance", base.new)
base.extend(self)
end
def new
raise "can't new in singleton class"
end
def instance
instance_variable_get("@instance")
end
end
使用和测试如下:
# test for singleton
class Test
include Singleton
end
a, b = Test.instance, Test.instance
puts a==b #=>true