tl;dr Inheriting from BasicObject allows you to build proxy objects that delegate methods like
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
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
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
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
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
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
annoying_fido didn’t bark.
As you can see,
AnnoyingDog did not invoke
#method_missing. This is because
AnnoyingDog inherits from
Object, which implements
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
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
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.