1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
|
# encoding: utf-8
module Memoizable
# Build the memoized method
class MethodBuilder
# Raised when the method arity is invalid
class InvalidArityError < ArgumentError
# Initialize an invalid arity exception
#
# @param [Module] descendant
# @param [Symbol] method
# @param [Integer] arity
#
# @api private
def initialize(descendant, method, arity)
super("Cannot memoize #{descendant}##{method}, its arity is #{arity}")
end
end # InvalidArityError
# Raised when a block is passed to a memoized method
class BlockNotAllowedError < ArgumentError
# Initialize a block not allowed exception
#
# @param [Module] descendant
# @param [Symbol] method
#
# @api private
def initialize(descendant, method)
super("Cannot pass a block to #{descendant}##{method}, it is memoized")
end
end # BlockNotAllowedError
# The original method before memoization
#
# @return [UnboundMethod]
#
# @api public
attr_reader :original_method
# Initialize an object to build a memoized method
#
# @param [Module] descendant
# @param [Symbol] method_name
# @param [#call] freezer
#
# @return [undefined]
#
# @api private
def initialize(descendant, method_name, freezer)
@descendant = descendant
@method_name = method_name
@freezer = freezer
@original_visibility = visibility
@original_method = @descendant.instance_method(@method_name)
assert_arity(@original_method.arity)
end
# Build a new memoized method
#
# @example
# method_builder.call # => creates new method
#
# @return [MethodBuilder]
#
# @api public
def call
remove_original_method
create_memoized_method
set_method_visibility
self
end
private
# Assert the method arity is zero
#
# @param [Integer] arity
#
# @return [undefined]
#
# @raise [InvalidArityError]
#
# @api private
def assert_arity(arity)
if arity.nonzero?
fail InvalidArityError.new(@descendant, @method_name, arity)
end
end
# Remove the original method
#
# @return [undefined]
#
# @api private
def remove_original_method
name = @method_name
@descendant.module_eval { undef_method(name) }
end
# Create a new memoized method
#
# @return [undefined]
#
# @api private
def create_memoized_method
name, method, freezer = @method_name, @original_method, @freezer
@descendant.module_eval do
define_method(name) do |&block|
fail BlockNotAllowedError.new(self.class, name) if block
memoized_method_cache.fetch(name) do
freezer.call(method.bind(self).call)
end
end
end
end
# Set the memoized method visibility to match the original method
#
# @return [undefined]
#
# @api private
def set_method_visibility
@descendant.send(@original_visibility, @method_name)
end
# Get the visibility of the original method
#
# @return [Symbol]
#
# @api private
def visibility
if @descendant.private_method_defined?(@method_name) then :private
elsif @descendant.protected_method_defined?(@method_name) then :protected
else :public
end
end
end # MethodBuilder
end # Memoizable
|