Delegation with BasicObject
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.
1
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:
1
2
3
4
5
6
7
8
9
10
11
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:
1
2
3
4
5
6
7
8
9
10
11
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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:
1
2
3
4
5
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.
1
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.
1
2
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.
1
2
- class AnnoyingDog
+ class AnnoyingDog < BasicObject
And let’s try our previous example again:
1
2
3
4
5
6
7
8
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.