Have you ever heard of SimpleDelegator
? It’s a pretty useful tool; it’s basically used to extend an instance’s behaviour with more methods without modifying its base class (extension is good for code maintainability!).
For example, imagine that you have a class like this:
class User
attr_accessor :first_name, :last_name
def initialize(first_name, last_name)
self.first_name = first_name
self.last_name = last_name
end
end
Now, if you want add a new method to a User
that won’t be used in all contexts (for example, only in views), you shouldn’t do it. Why taint your wonderful class with ugly new code that will extend your to 1000 lines? Instead, just create a new delegator class.
class PrintableUser < SimpleDelegator
def full_name
"#{first_name} #{last_name}"
end
end
And use it like this:
user = User.new 'Daniel', 'Herzog'
printable = PrintableUser.new(user)
puts user.full_name
SimpleDelegator
’s internal implementation is actually very simple (and inneficient): it redefines the method_missing
method to lookup on the delegated object. This means that multiple delegators may be chainned to implement multiple features:
require 'delegate'
class A < SimpleDelegator
end
class B < SimpleDelegator
end
class C < SimpleDelegator
end
class Original
end
obj = C.new(B.new(A.new(Original.new)))
puts obj.class # C
Crazy, right? But there is a problem: obj
is no longer an Original
instance, but a C
instance which delegates to B
, which delegates to A
, which delegates to Original
. This can be a problem when passing delegated objects to other methods without checking it’s class (duck typing rules!). For example, on active-record, you can save an object to the database if it refers a delegated object:
class User < ActiveRecord::Base
belongs_to :group
end
class Group < ActiveRecord::Base
has_many :users
end
class PrintableUser < SimpleDelegator
# ...
end
# ...
user = User.create
printable_user = PrintableUser.new(user)
group = Group.last
group.users << printable_user # <- ¡ERROR! Unexpected PrintableUser type, expected User.
To fix this issue, you can implement an original
(or whatever name) method to retrieve the original object. To access a delegated object within the delegator, you need to use __getobj__
. Since self
refers to the current instance, you must iterate over all delegated objects until someone does not respond to __getobj__
:
class A < SimpleDelegator
end
class B < SimpleDelegator
end
class C < SimpleDelegator
end
class Original
def original
obj = self
obj = self.__getobj__ while obj.respond_to? :__getobj__
obj
end
end
obj = C.new(B.new(A.new(Original.new)))
puts obj.class # C
puts obj.original.class # Original
Tada! Also, you can create circular dependencies with delegated objects:
class A < SimpleDelegator
end
class B < SimpleDelegator
end
a = A.new(nil)
b = B.new(a)
a.__setobj__(b)
Leading to a SystemStackError: stack level too deep
everytime you use a
or b
. Be careful!!