class Module
Constants
- DELEGATION_RESERVED_KEYWORDS
- DELEGATION_RESERVED_METHOD_NAMES
- RUBY_RESERVED_KEYWORDS
Public Instance Methods
Provides a memoize_delegate
class method to easily expose contained objects' public methods as your own.
Options¶ ↑
-
:to
- Specifies the target object -
:prefix
- Prefixes the new method with the target name or a custom prefix -
:allow_nil
- if set to true, prevents aNoMethodError
from being raised
The macro receives one or more method names (specified as symbols or strings) and the name of the target object via the :to
option (also a symbol or string).
Delegation is particularly useful with Active Record associations:
class Greeter < ActiveRecord::Base def hello 'hello' end def goodbye 'goodbye' end end class Foo < ActiveRecord::Base belongs_to :greeter memoize_delegate :hello, to: :greeter end Foo.new.hello # => "hello" Foo.new.goodbye # => NoMethodError: undefined method `goodbye' for #<Foo:0x1af30c>
Multiple memoize_delegates to the same target are allowed:
class Foo < ActiveRecord::Base belongs_to :greeter memoize_delegate :hello, :goodbye, to: :greeter end Foo.new.goodbye # => "goodbye"
Methods can be memoize_delegated to instance variables, class variables, or constants by providing them as a symbols:
class Foo CONSTANT_ARRAY = [0,1,2,3] @@class_array = [4,5,6,7] def initialize @instance_array = [8,9,10,11] end memoize_delegate :sum, to: :CONSTANT_ARRAY memoize_delegate :min, to: :@@class_array memoize_delegate :max, to: :@instance_array end Foo.new.sum # => 6 Foo.new.min # => 4 Foo.new.max # => 11
It's also possible to memoize_delegate
a method to the class by using :class
:
class Foo def self.hello "world" end memoize_delegate :hello, to: :class end Foo.new.hello # => "world"
Delegates can optionally be prefixed using the :prefix
option. If the value is true
, the memoize_delegate
methods are prefixed with the name of the object being memoize_delegated to.
Person = Struct.new(:name, :address) class Invoice < Struct.new(:client) memoize_delegate :name, :address, to: :client, prefix: true end john_doe = Person.new('John Doe', 'Vimmersvej 13') invoice = Invoice.new(john_doe) invoice.client_name # => "John Doe" invoice.client_address # => "Vimmersvej 13"
It is also possible to supply a custom prefix.
class Invoice < Struct.new(:client) memoize_delegate :name, :address, to: :client, prefix: :customer end invoice = Invoice.new(john_doe) invoice.customer_name # => 'John Doe' invoice.customer_address # => 'Vimmersvej 13'
If the target is nil
and does not respond to the memoize_delegated method a NoMethodError
is raised, as with any other value. Sometimes, however, it makes sense to be robust to that situation and that is the purpose of the :allow_nil
option: If the target is not nil
, or it is and responds to the method, everything works as usual. But if it is nil
and does not respond to the memoize_delegated method, nil
is returned.
class User < ActiveRecord::Base has_one :profile memoize_delegate :age, to: :profile end User.new.age # raises NoMethodError: undefined method `age'
But if not having a profile yet is fine and should not be an error condition:
class User < ActiveRecord::Base has_one :profile memoize_delegate :age, to: :profile, allow_nil: true end User.new.age # nil
Note that if the target is not nil
then the call is attempted regardless of the :allow_nil
option, and thus an exception is still raised if said object does not respond to the method:
class Foo def initialize(bar) @bar = bar end memoize_delegate :name, to: :@bar, allow_nil: true end Foo.new("Bar").name # raises NoMethodError: undefined method `name'
The target method must be public, otherwise it will raise NoMethodError
.
# File lib/memoize_delegate/delegation.rb, line 153 def memoize_delegate(*methods, to: nil, prefix: nil, allow_nil: nil) unless to raise ArgumentError, 'Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. memoize_delegate :hello, to: :greeter).' end if prefix == true && to =~ /^[^a-z_]/ raise ArgumentError, 'Can only automatically set the delegation prefix when delegating to a method.' end method_prefix = \ if prefix "#{prefix == true ? to : prefix}_" else '' end location = caller_locations(1, 1).first file, line = location.path, location.lineno to = to.to_s to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to) methods.each do |method| # Attribute writer methods only accept one argument. Makes sure []= # methods still accept two arguments. definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block' # The following generated method calls the target exactly once, storing # the returned value in a dummy variable. # # Reason is twofold: On one hand doing less calls is in general better. # On the other hand it could be that the target has side-effects, # whereas conceptually, from the user point of view, the delegator should # be doing one call. if allow_nil method_def = [ "def #{method_prefix}#{method}(#{definition})", "_ = #{to}", "if !_.nil? || nil.respond_to?(:#{method})", " if instance_variable_get('@_memoize_delegate_#{to}_#{method}')", " instance_variable_get('@_memoize_delegate_#{to}_#{method}')", " else", " instance_variable_set('@_memoize_delegate_#{to}_#{method}', _.#{method}(#{definition}))", " end", "end", "end" ].join ';' else exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} memoize_delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") method_def = [ "def #{method_prefix}#{method}(#{definition})", " _ = #{to}", " if instance_variable_get('@_memoize_delegate_#{to}_#{method}')", " instance_variable_get('@_memoize_delegate_#{to}_#{method}')", " else", " instance_variable_set('@_memoize_delegate_#{to}_#{method}', _.#{method}(#{definition}))", " end", "rescue NoMethodError => e", " if _.nil? && e.name == :#{method}", " #{exception}", " else", " raise", " end", "end" ].join ';' end module_eval(method_def, file, line) end end