Delegation with BasicObject
Using BasicObject to build proxy objects
by Ray Zane
tl;dr Inheriting from BasicObject allows you to build proxy objects that delegate methods like #inspect
, #to_s
, and #is_a?
.
If you're familiar with Ruby, you probably know that Object
is the default root of all Ruby objects. So it's probably a top-level class, right? Nope. Object
inherits from BasicObject
.
Object.superclass #=> BasicObject
In this article, we'll explore how you can use BasicObject
to implement proxy objects that delegate all of their methods to another class.
A Delegate Situation
Let's imagine you have a class called Dog
:
class Dog
attr_reader :name
def initialize(name)
@name = name
end
def bark
puts 'WOOF!'
end
end
Now, imagine that you want to call the #bark
method whenever any method is called on an instance of Dog
.
Surely, you could use inheritance to override every single method like so:
class DogSubclass < Dog
def name
bark
super
end
end
dog = DogSubclass.new('Fido')
dog.name
# WOOF!
#=> "Fido"
Great. That will solve our problem for now. But, what if Dog
had hundreds of methods? Would you manually override every single method that Dog
defines?
The Proxy Pattern
A more clever way to implement this feature would be to create a proxy. In short, a proxy forwards (or delegates) all method invocations to another object.
class AnnoyingDog
def initialize(dog)
@dog = dog
end
private
def method_missing(meth, *args, &block)
@dog.bark
@dog.send(meth, *args, &block)
end
end
fido = Dog.new('Fido')
fido.name
#=> "Fido"
annoying_fido = AnnoyingDog.new(fido)
annoying_fido.name
# WOOF!
#=> "Fido"
When we call annoying_fido.name
, Ruby knows that AnnoyingDog
doesn't define a #name
method. Therefore, #method_missing
is dispatched. This is where we can make the dog #bark
and delegate the #name
method.
Let's try another example:
fido.to_s
#=> "#<Dog:0x007fb3528d5a60>"
annoying_fido.to_s
#=> "#<AnnoyingDog:0x007f998a04ee28>"
We've got two problems. First, AnnoyingDog#to_s
returns a different result than Dog#to_s
. Second, annoying_fido
didn't bark.
As you can see, AnnoyingDog
did not invoke #method_missing
. This is because AnnoyingDog
inherits from Object
, which implements #to_s
.
AnnoyingDog.instance_methods.include?(:to_s) #=> true
Back to Basics
As of Ruby 2.3, Object
implements a total of 56 methods. To the contrary, BasicObject
is extremely lightweight, containing just 8 critical methods.
BasicObject.instance_methods
#=> [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__, :__id__, :__binding__]
Now that we've identified the problem, let's try making AnnoyingDog
inherit from BasicObject
.
- class AnnoyingDog
+ class AnnoyingDog < BasicObject
And let's try our previous example again:
fido = Dog.new('Fido')
fido.to_s
#=> "#<Dog:0x007fb9841d9990>"
annoying_fido = AnnoyingDog.new(fido)
annoying_fido.to_s
# WOOF!
#=> "#<Dog:0x007fb06284ed28>"
If it barks like a dog, it must be a dog. The #to_s
method is no longer defined for AnnoyingDog
. Therefore, annoying_fido
barks and behaves exactly like fido
, but with slightly different behavior.
The Real World
If you're interested in seeing BasicObject in action, take a look at Baby Squeel.