gears

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.