ruby

Changing the Way Ruby Creates Objects

Benedikt Deicke

Benedikt Deicke on

Changing the Way Ruby Creates Objects

One of the things that makes Ruby great is that we can customize almost anything to our needs. This is both useful and dangerous. It's easy to shoot ourselves in the foot, but when used carefully, this can result in pretty powerful solutions.

At Ruby Magic, we think useful and dangerous is an excellent combination. Let's look at how Ruby creates and initializes objects and how we can modify the default behavior.

The Basics of Creating New Objects from Classes

To get started, let's see how to create objects in Ruby. To create a new object (or instance), we call new on the class. Unlike other languages, new isn't a keyword of the language itself, but a method that gets called just like any other.

1class Dog
2end
3
4object = Dog.new

In order to customize the newly created object, it is possible to pass arguments to the new method. Whatever is passed as arguments, will get passed to the initializer.

1class Dog
2  def initialize(name)
3    @name = name
4  end
5end
6
7object = Dog.new('Good boy')

Again, unlike other languages, the initializer in Ruby is also just a method instead of some special syntax or keyword.

With that in mind, shouldn't it be possible to mess around with those methods, just like it is possible with any other Ruby method? Of course it is!

Modifying the Behavior of a Single Object

Let's say we want to ensure that all objects of a particular class will always print log statements, even if the method is overridden in subclasses. One way to do this is to add a module to the object's singleton class.

1module Logging
2  def make_noise
3    puts "Started making noise"
4    super
5    puts "Finished making noise"
6  end
7end
8
9class Bird
10  def make_noise
11    puts "Chirp, chirp!"
12  end
13end
14
15object = Bird.new
16object.singleton_class.include(Logging)
17object.make_noise
18# Started making noise
19# Chirp, chirp!
20# Finished making noise

In this example, a Bird object is created using Bird.new, and the Logging module is included in the resulting object using its singleton class.

What's a Singleton Class?

Ruby allows methods that are unique to a single object. To support this, Ruby adds an anonymous class between the object and its actual class. When methods are called, the ones defined on the singleton class get precedence over the methods in the actual class. These singleton classes are unique to every object, so adding methods to them doesn't affect any other objects of the actual class. Learn more about classes and objects in the Programming Ruby guide.

It's a bit cumbersome to modify the singleton class of each object whenever it is created. So let's move the inclusion of the Logging class to the initializer to add it for every created object.

1module Logging
2  def make_noise
3    puts "Started making noise"
4    super
5    puts "Finished making noise"
6  end
7end
8
9class Bird
10  def initialize
11    singleton_class.include(Logging)
12  end
13
14  def make_noise
15    puts "Chirp, chirp!"
16  end
17end
18
19object = Bird.new
20object.make_noise
21# Started making noise
22# Chirp, chirp!
23# Finished making noise

While this works well, if we create a subclass of Bird, like Duck, its initializer needs to call super to retain the Logging behavior. While one can argue that it's always a good idea to properly call super whenever a method is overridden, let's try to find a way that doesn't require it.

If we don't call super from the subclass, we lose the inclusion of the Logger class:

1class Duck < Bird
2  def initialize(name)
3    @name = name
4  end
5
6  def make_noise
7    puts "#{@name}: Quack, quack!"
8  end
9end
10
11object = Duck.new('Felix')
12object.make_noise
13# Felix: Quack, quack!

Instead, Let's override Bird.new. As mentioned before, new is just a method implemented on classes. So we can override it, call super, and modify the newly created object to our needs.

1class Bird
2  def self.new(*arguments, &block)
3    instance = super
4    instance.singleton_class.include(Logging)
5    instance
6  end
7end
8
9object = Duck.new('Felix')
10object.make_noise
11# Started making noise
12# Felix: Quack, quack!
13# Finished making noise

But, what happens when we call make_noise in the initializer? Unfortunately, because the singleton class doesn't include the Logging module yet, we won't get the desired output.

Luckily, there's a solution: It's possible to create the default .new behavior from scratch by calling allocate.

1class Bird
2  def self.new(*arguments, &block)
3    instance = allocate
4    instance.singleton_class.include(Logging)
5    instance.send(:initialize, *arguments, &block)
6    instance
7  end
8end

Calling allocate returns a new, uninitialized object of the class. So afterward, we can include the additional behavior and only then, call the initialize method on that object. (Because initialize is private by default, we have to resort to using send for this).

The Truth About Class#allocate

Unlike other methods, it's not possible to override allocate. Ruby doesn't use the conventional way of dispatching methods for allocate internally. As a result, just overriding allocate without also overriding new doesn't work. However, if we're calling allocate directly, Ruby will call the redefined method. Learn more about Class#new and Class#allocate in Ruby's documentation.

Why Would We Do This?

As with a lot of things, modifying the way Ruby creates objects from classes can be dangerous and things might break in unexpected ways.

Nonetheless, there are valid use cases for changing the object creation. For instance, ActiveRecord uses allocate with a different init_from_db method to change the initialization process when creating objects from the database as opposed to building unsaved objects. It also uses allocate to convert records between different single-table inheritance types with becomes.

Most important, by playing around with object creation, you get a deeper insight into how it works in Ruby and open your mind to different solutions. We hope you enjoyed the article.

We'd love to hear about the things you implemented by changing Ruby's default way of creating objects. Please don't hesitate to tweet your thoughts to @AppSignal.

Share this article

RSS
Benedikt Deicke

Benedikt Deicke

Guest author Benedikt Deicke is a software engineer and CTO of Userlist. On the side, he’s writing a book about building SaaS applications in Ruby on Rails.

All articles by Benedikt Deicke

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps