# 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
Copyright 2K16 - 2K18 Indonesian Hacker Rulez