Implementing inheritance with params: CreateProducts < ActiveRecord::Migration[5.0]

Subscribe to receive new articles. No spam. Quality content.

In Rails 5 each migration class is inherited from ActiveRecord::Migration[5.0]. It looks quite unusual to see that [5.0] part at the end of the parent class. In this article I'll describe why we might need it and how it works.

It all started from the message in dry-rb channel on Gitter. Somebody asked:

hello, can anyone give me some pointers on how is this array syntax implemented?

class UserRepo < ROM::Repository[:users]

Yes, inheritance with params is being used not just in Rails, but in ROM too :)

So I started to dig into this topic and it's quite interesting. One more time I was amazed how flexible Ruby is.

Examples

Basic inheritance:

class Human; end

class Man < Human
end

Also, we can do something like this:

class Human; end

foo = Human

class Man < foo 
end

Or this:

class Human; end

def parent_class
  Human
end

class Man < parent_class
end

As long as Ruby code after < evaluates to a class object - it works.

Interesting, let's try to put it this way:

class Human
  def self.[](version)
    puts version
    self
  end
end

class Man < Human[:basic]
end

It prints basic. Ruby executed this code Human[:basic], called class method Human.[] and passed basic as version variable.

One thing to notice that Ruby executes this code immediately. I didn't create any object of that class, but it still executed self.[](version) method.

Rails use this param to pass version of migration, in case of ROM gem, they use it to pass root ancestor, so this code:

class UserRepo < ROM::Repository[:users]

Is shorter version of the following:

class UserRepo < ROM::Repository::Root
  root :users
end

And if we go to implementation we will see this:

def [](name)
  klass = Class.new(self < Repository::Root ? self : Repository::Root)
  klass.root(name)
  klass
end

Indeed, they get the name param and assign it as a root.

It's quite simple, right? Just add class method [] that returns a class object and you have it, inheritance with params :)

More fun

I wanted to show couple more interesting things that Ruby offers to us. Did you know that class declaration in Ruby returns value?

result = class Foo; end
result.inspect # => nil

It returns nil for empty class, or symbol with the name of the latest defined method:

result = class Foo
  def bar
    5
  end

  def baz
    10
  end
end

result.inspect # => :baz

But we can return custom value as well:

result = class Foo
  def bar
    10
  end

  42
end

result.inspect # => 42

The other interesting moment that classes don't have a name until you assign class object to a constant, it might sound hard to understand so let's check this example:

klass = Class.new # => #<Class:0x007fb14e036ba8>

Now it's just an object of class Class, but as soon as we assign this object to a constant:

Man = klass

It's not a #<Class:0x007fb14e036ba8> anymore:

puts klass # => Man

If your'e interested why it behaves like this, you can read "Metaprogramming Ruby" book which describes this topic really well.

Thanks for reading, now you know how to implement inheritance with params and couple more interesting tricks in Ruby!

Subscribe to receive new articles. No spam. Quality content.

Comments