Hi--
I would like to propose that the Multiton pattern be supported in
Ruby's standard library. I believe the Multiton module originally
developed by Ara T. Howard and improved by myself is pretty solid at
this point. So I would like to offer it for the purpose.
The code follows for others to consider. And please let me know if you
have any suggestions for improving it further.
Thanks,
Trans
---
# = Multiton
#
# Multiton module that ensures only one object to be allocated for a given
# argument list.
#
# The 'multiton' pattern is similar to a singleton, but instead of only one
# instance, there are several similar instances. It is useful when you want to
# avoid constructing objects many times because of some huge expense (connecting
# to a database for example), require a set of similar but not identical
# objects, and cannot easily control how many times a contructor may be called.
#
# == Synopsis
#
# class SomeMultitonClass
# include Multiton
# attr :arg
# def initialize(arg)
# @arg = arg
# end
# end
#
# a = SomeMultitonClass.new(4)
# b = SomeMultitonClass.new(4) # a and b are same object
# c = SomeMultitonClass.new(2) # c is a different object
#
# == Previous Behavior
#
# In previous versions of Multiton the #new method was made
# private and #instance was used in its stay --just like Singleton.
# But this has proved less desirable for Multiton since Multitions can
# have multiple instances, not just one.
#
# So instead Multiton now defines #create as a private alias of
# the original #new method (just in case it is needed) and then
# defines #new to handle the multiton, and #instance is provided
# as an alias for it.
#
# == How It Works
#
# A pool of objects is searched for a previously cached object,
# if one is not found we construct one and cache it in the pool
# based on class and the args given to the contructor. To ensure
# equvalence, arguments are round-tripeed through Marshal. So
# only objects that can be marshalled can serve as arguments for
# the cache.
#
# Another limitation is that it is impossible to detect if different
# blocks were given to a contructor (if it takes a block). So it
# is the constructor arguments _only_ which determine the uniqueness
# of an object. To workaround this, define the _class_ method
# ::multiton_id.
#
# def Klass.multiton_id(*args, &block)
# # ...
# end
#
# Which should return a hash key used to identify the object being
# constructed as (not) unique.
module Multiton
# Pools of objects cached on class type.
POOLS = {}
# Method which can be defined by a class to determine object uniqueness.
MULTITON_ID_HOOK = :multiton_id
# Method which can be defined by a class to create multiton objects.
MULTITON_NEW_HOOK = :multiton_new
def self.append_features( klass )
class << klass
unless method_defined?(MULTITON_NEW_HOOK)
alias_method MULTITON_NEW_HOOK, :new
end
# Define #instance method. This will become #new.
def instance(*args, &block)
# if the class defined 'multiton_id' we use this as the key
# otherwise we simply use the argument list
k = begin
send MULTITON_ID_HOOK, *args, &block
rescue NameError
args
end
# marshal the args to serve as cache key (b/c hashes fail as this)
k = begin
Marshal.dump(k)
rescue TypeError
k
end
# Return cached object. Set if first occurance.
unless obj = (POOLS[self] ||= {})[k]
begin
critical = Thread.critical
Thread.critical = true
obj = (POOLS[self][k] = send(MULTITON_NEW_HOOK, *args, &block))
ensure
Thread.critical = critical # restore state
end
end
return obj
end
alias_method :create, :new
alias_method :new, :instance
end
end
## DON'T NEED THANKS TO MARSHAL
# Recursive dehash function. This converts hash into associative
# arrays, and any hashes that contains, etc.
#def self.dehash(arg)
# case arg
# when Hash
# arg.to_a.collect{|a| dehash(a)}
# when Array
# arg.collect{|a| dehash(a)}
# else
# arg
# end
#end
end
# _____ _
# |_ _|__ ___| |_
# | |/ _ \/ __| __|
# | | __/\__ \ |_
# |_|\___||___/\__|
#
=begin test
require 'test/unit'
#
# EXAMPLE A - STANDARD USAGE
#
class TC_Multiton_A < Test::Unit::TestCase
class SomeMultitonClass
include Multiton
attr :arg
def initialize(arg)
@arg = arg
end
end
def test_standard
a = SomeMultitonClass.instance(4)
b = SomeMultitonClass.instance(4) # a and b are same object
c = SomeMultitonClass.instance(2) # c is a different object
assert_equal( a, b )
assert_equal( 42, [a.arg,b.arg].max * 10 + c.arg )
end
end
#
# EXAMPLE B - MODIFY AN EXISTING CLASS, SHARED FILEHANDLES
#
class TC_Multiton_B < Test::Unit::TestCase
class ::File
include Multiton
end
def test_modify_existing
lineno = __LINE__
# HERE1
# HERE2
a = File.instance(__FILE__)
b = File.instance(__FILE__)
assert_equal( a, b )
lineno.times{ a.gets }
assert_equal( "# HERE1", a.gets.strip )
assert_equal( "# HERE2", b.gets.strip )
end
end
#
# EXAMPLE C - INHERITENCE
#
class TC_Multiton_C < Test::Unit::TestCase
class A < String
include Multiton
end
# B is also a multiton - with it's OWN object cache
class B < A; end
def test_inheritence
# w is the same object as x, y is the same object as z
w,x,y,z = A.instance('A'), A.instance('A'), B.instance('B'),
B.instance('B')
assert_equal( w.object_id, x.object_id ) # -> true
assert_equal( y.object_id, z.object_id ) # -> true
a = B.instance('A')
b = B.instance('A')
assert_not_equal( a.object_id, w.object_id ) # -> false (each
class has it's own pool)
assert_equal( a.object_id, b.object_id ) # -> true
end
end
#
# EXAMPLE D - MULTIPLE MODULE INCLUSION (does nothing)
#
class TC_Multiton_D < Test::Unit::TestCase
class A < String
include Multiton
end
# B is also a multiton - with it's OWN object cache
class B < A; end
def test_multiple
# w is the same object as x, y is the same object as z
w,x,y,z = A.instance('A'), A.instance('A'), B.instance('B'),
B.instance('B')
yz_id = y.object_id || z.object_id
B.class_eval {
include Multiton
}
# y is not only the same object as z, but they are both the same object(s)
# as from EXAMPLE C
y,z = B.instance('B'), B.instance('B')
assert_equal( y.object_id, yz_id ) # -> true
assert_equal( z.object_id, yz_id ) # -> true
end
end
#
# EXAMPLE E - SO YOU WANNA USE NEW INSTEAD OF INSTANCE...
#
class TC_Multiton_E < Test::Unit::TestCase
module K
# use an inner class which is itself a multiton
class K < String; include Multiton; end
# define a new which returns a mutltion using #instance...
class << self
def new(*args, &block)
K.instance *args, &block
end
end
end
def test_new
the = K.new '4'
answer = K.new '2'
assert_equal( "42", sprintf( "%s%s", the, answer ) ) #-> 42
assert_equal( K::K, the.class ) #-> K::K
end
end
#
# EXAMPLE F - using Klass.multiton_id
#
class TC_Multiton_F < Test::Unit::TestCase
class Klass
include Multiton
def initialize( important, not_important )
@important, @not_important = important, not_important
end
def Klass.multiton_id(*args, &block)
# we consider only the first arg
important, not_important = *args
important
end
end
def test_using_id
a = Klass.instance( :a, :b )
b = Klass.instance( :a, :c )
c = Klass.instance( :b, :b )
assert_equal( a, b )
assert_not_equal( a, c )
assert_not_equal( b, c )
end
end
=end
# Author:: Ara T. Howard, Thomas Sawyer
# Copyright:: Copyright (c) 2005-2007 Ara T. Howard, Thomas Sawyer
# License:: Ruby License