#-- # Multiton # # Copyright (c) 2005 Florian Frank # # Ruby License # # This module is free software. You may use, modify, and/or redistribute this # software under the same terms as Ruby. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. # # # ========================================================================== # Revision History :: # -------------------------------------------------------------------------- # 5.8 trans * Minor modifications to documentation. # 5.2 trans * Added New and Semi variations. # ========================================================================== #++ #:title: 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's usefull when you want to # avoid constructing objects many times because of some huge expence (connecting # to a database for example), require a set of similar but not identical # objects, and cannot easily control how many time a contructor may be called. # # == Synopsis # # require 'mega/multiton' # # class SomeMultitonClass # include Multiton # attr :arg # def initialize(arg) # @arg = arg # end # end # # a = SomeMultitonClass.instance(4) # b = SomeMultitonClass.instance(4) # a and b are same object # c = SomeMultitonClass.instance(2) # c is a different object # # In previous versions the Class.new method was made private, but for # Semi Multitons this restriction has been lifted with the following # caveat: Class.instance will attempt to retreive a previously cached # object, constructing one if needed, but Class.new will *always* # construct a new object - this should allow great flexiblity with # multiton classes. # # == How It Works # # We try to use 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 # # note: a limitation of this approach is that it is impossible to # detect if different blocks were given to a contructor (if it takes a # block. Thus, it is the constructor *args _exclusively_ which determine # the uniqueness of an object. A workaround is for the class to define # the _class_ method multiton_id, eg: # # def Klass.multiton_id(*args, &block) # # ... # end # # Which should return a hash key used to identify the object being # constructed as (not) unique. # # == Author(s) # # * Thomas Sawyer # 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) klass.private_class_method(:new) def klass.instance(*args, &block) # if the class defined 'multiton_id' we use this as the key # otherwise we simply use the argument list. k = (respond_to?(MULTITON_ID_HOOK) ? send(MULTITON_ID_HOOK, *args, &block) : args) unless (obj = (POOLS[self] ||= {})[k]) begin critical = Thread.critical Thread.critical = true meth = self.respond_to?(MULTITON_NEW_HOOK) ? MULTITON_NEW_HOOK : :new obj = (POOLS[self][k] = self.send(meth, *args, &block)) ensure Thread.critical = critical # restore state end end return obj end end # A New-Multiton uses the #new method rather than the #instance method. # (see Multiton) module New def self.append_features(klass) def klass.new(*args, &block) # if the class defined 'multiton_id' we use this as the key # otherwise we simply use the argument list. k = (respond_to?(MULTITON_ID_HOOK) ? send(MULTITON_ID_HOOK, *args, &block) : args) unless (obj = (POOLS[self] ||= {})[k]) begin critical = Thread.critical Thread.critical = true if self.respond_to?(MULTITON_NEW_HOOK) obj = (POOLS[self][k] = self.send(MULTITON_NEW_HOOK, *args, &block)) else obj = super end ensure Thread.critical = critical # restore state end end return obj end end end # A Semi-Multiton allows for both regular instantiation and multiton use. In other words # the #new method is left accessible. # (see Multiton) module Semi def self.append_features(klass) def klass.instance(*args, &block) # if the class defined 'multiton_id' we use this as the key # otherwise we simply use the argument list. k = (respond_to?(MULTITON_ID_HOOK) ? send(MULTITON_ID_HOOK, *args, &block) : args) unless (obj = (POOLS[self] ||= {})[k]) begin critical = Thread.critical Thread.critical = true meth = self.respond_to?(MULTITON_NEW_HOOK) ? MULTITON_NEW_HOOK : :new obj = (POOLS[self][k] = self.send(meth, *args, &block)) ensure Thread.critical = critical # restore state end end return obj end end end end # _____ _ # |_ _|__ ___| |_ # | |/ _ \/ __| __| # | | __/\__ \ |_ # |_|\___||___/\__| # =begin testing 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