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 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
|
# Adaptable is a mix-in module that adds adaptability to a class.
# This means that an adapter can
# associate itself with an instance of the class and store additional data/have behavior.
#
# This mechanism should be used when there is a desire to keep implementation concerns separate.
# In Ruby it is always possible to open and modify a class or instance to teach it new tricks, but it
# is however not possible to do this for two different versions of some service at the same time.
# The Adaptable pattern is also good when only a few of the objects of some class needs to have extra
# information (again possible in Ruby by adding instance variables dynamically). In fact, the implementation
# of Adaptable does just that; it adds an instance variable named after the adapter class and keeps an
# instance of this class in this slot.
#
# @note the implementation details; the fact that an instance variable is used to keep the adapter
# instance data should not
# be exploited as the implementation of _being adaptable_ may change in the future.
# @api private
#
module Puppet::Pops
module Adaptable
# Base class for an Adapter.
#
# A typical adapter just defines some accessors.
#
# A more advanced adapter may need to setup the adapter based on the object it is adapting.
# @example Making Duck adaptable
# class Duck
# include Puppet::Pops::Adaptable
# end
# @example Giving a Duck a nick name
# class NickNameAdapter < Puppet::Pops::Adaptable::Adapter
# attr_accessor :nick_name
# end
# d = Duck.new
# NickNameAdapter.adapt(d).nick_name = "Daffy"
# NickNameAdapter.get(d).nick_name # => "Daffy"
#
# @example Giving a Duck a more elaborate nick name
# class NickNameAdapter < Puppet::Pops::Adaptable::Adapter
# attr_accessor :nick_name, :object
# def initialize o
# @object = o
# @nick_name = "Yo"
# end
# def nick_name
# "#{@nick_name}, the #{o.class.name}"
# end
# def NickNameAdapter.create_adapter(o)
# x = new o
# x
# end
# end
# d = Duck.new
# n = NickNameAdapter.adapt(d)
# n.nick_name # => "Yo, the Duck"
# n.nick_name = "Daffy"
# n.nick_name # => "Daffy, the Duck"
# @example Using a block to set values
# NickNameAdapter.adapt(o) { |a| a.nick_name = "Buddy!" }
# NickNameAdapter.adapt(o) { |a, o| a.nick_name = "You're the best #{o.class.name} I met."}
#
class Adapter
# Returns an existing adapter for the given object, or nil, if the object is not
# adapted.
#
# @param o [Adaptable] object to get adapter from
# @return [Adapter<self>] an adapter of the same class as the receiver of #get
# @return [nil] if the given object o has not been adapted by the receiving adapter
# @raise [ArgumentError] if the object is not adaptable
#
def self.get(o)
attr_name = self_attr_name
o.instance_variable_get(attr_name)
end
# Returns an existing adapter for the given object, or creates a new adapter if the
# object has not been adapted, or the adapter has been cleared.
#
# @example Using a block to set values
# NickNameAdapter.adapt(o) { |a| a.nick_name = "Buddy!" }
# NickNameAdapter.adapt(o) { |a, o| a.nick_name = "Your the best #{o.class.name} I met."}
# @overload adapt(o)
# @overload adapt(o, {|adapter| block})
# @overload adapt(o, {|adapter, o| block})
# @param o [Adaptable] object to add adapter to
# @yieldparam adapter [Adapter<self>] the created adapter
# @yieldparam o [Adaptable] optional, the given adaptable
# @param block [Proc] optional, evaluated in the context of the adapter (existing or new)
# @return [Adapter<self>] an adapter of the same class as the receiver of the call
# @raise [ArgumentError] if the given object o is not adaptable
#
def self.adapt(o, &block)
attr_name = self_attr_name
value = o.instance_variable_get(attr_name)
adapter = value || associate_adapter(create_adapter(o), o)
if block_given?
if block.arity == 1
block.call(adapter)
else
block.call(adapter, o)
end
end
adapter
end
# Creates a new adapter, associates it with the given object and returns the adapter.
#
# @example Using a block to set values
# NickNameAdapter.adapt_new(o) { |a| a.nick_name = "Buddy!" }
# NickNameAdapter.adapt_new(o) { |a, o| a.nick_name = "Your the best #{o.class.name} I met."}
# This is used when a fresh adapter is wanted instead of possible returning an
# existing adapter as in the case of {Adapter.adapt}.
# @overload adapt_new(o)
# @overload adapt_new(o, {|adapter| block})
# @overload adapt_new(o, {|adapter, o| block})
# @yieldparam adapter [Adapter<self>] the created adapter
# @yieldparam o [Adaptable] optional, the given adaptable
# @param o [Adaptable] object to add adapter to
# @param block [Proc] optional, evaluated in the context of the new adapter
# @return [Adapter<self>] an adapter of the same class as the receiver of the call
# @raise [ArgumentError] if the given object o is not adaptable
#
def self.adapt_new(o, &block)
adapter = associate_adapter(create_adapter(o), o)
if block_given?
if block.arity == 1
block.call(adapter)
else
block.call(adapter, o)
end
end
adapter
end
# Clears the adapter set in the given object o. Returns any set adapter or nil.
# @param o [Adaptable] the object where the adapter should be cleared
# @return [Adapter] if an adapter was set
# @return [nil] if the adapter has not been set
#
def self.clear(o)
attr_name = self_attr_name
if o.instance_variable_defined?(attr_name)
o.send(:remove_instance_variable, attr_name)
else
nil
end
end
# This base version creates an instance of the class (i.e. an instance of the concrete subclass
# of Adapter). A Specialization may want to create an adapter instance specialized for the given target
# object.
# @param o [Adaptable] The object to adapt. This implementation ignores this variable, but a
# specialization may want to initialize itself differently depending on the object it is adapting.
# @return [Adapter<self>] instance of the subclass of Adapter receiving the call
#
def self.create_adapter(o)
new
end
# Associates the given adapter with the given target object
# @param adapter [Adapter] the adapter to associate with the given object _o_
# @param o [Adaptable] the object to adapt
# @return [adapter] the given adapter
#
def self.associate_adapter(adapter, o)
o.instance_variable_set(self_attr_name, adapter)
adapter
end
# Returns a suitable instance variable name given a class name.
# The returned string is the fully qualified name of a class with '::' replaced by '_' since
# '::' is not allowed in an instance variable name.
# @param name [String] the fully qualified name of a class
# @return [String] the name with all '::' replaced by '_'
# @api private
#
def self.instance_var_name(name)
name.split(DOUBLE_COLON).join(USCORE)
end
# Returns the name of the class, or the name of the type if the class represents an Object type
# @return [String] the name of the class or type
def self.type_name
self.name
end
# Returns a suitable instance variable name for the _name_ of this instance. The name is created by calling
# Adapter#instance_var_name and then cached.
# @return [String] the instance variable name for _name_
# @api private
#
def self.self_attr_name
@attr_name_sym ||= :"@#{instance_var_name(type_name)}"
end
end
end
end
|