I recently received a comment on my STI tutorial asking how to keep all the subclasses in one file. Indeed, creating one file for one line of code is a bit overkill but that’s the way Rails works: Conventions over configuration.
However, I thought it was interesting to talk about 2 ways to avoid creating new files.
Initial Code
If you remember correctly, in the first part of the STI tutorial, we had the following:
# app/models/animal.rb
class Animal < ActiveRecord::Base
belongs_to :tribe
self.inheritance_column = :race
# We will need a way to know which animals
# will subclass the Animal model
def self.races
%w(Lion WildBoar Meerkat)
end
end
# app/models/lion.rb
class Lion < Animal; end
# app/models/meerkat.rb
class Meerkat < Animal; end
# app/models/wild_boar.rb
class WildBoar < Animal; end
We had to split the subclasses in different files to use Rails autoloading. Without it, the classes Lion, Meerkat and WidlBoar were not available until we made at least one call to Animal. Let’s see how we can keep everything in the same file.
1. Autoload
Here’s the code we want to fix. If you use the following, start a rails console and try to access Lion, you will get an undefined error.
# app/models/animal.rb
class Animal < ActiveRecord::Base
belongs_to :tribe
self.inheritance_column = :race
# We will need a way to know which animals
# will subclass the Animal model
def self.races
%w(Lion WildBoar Meerkat)
end
end
class Lion < Animal; end
class Meerkat < Animal; end
class WildBoar < Animal; end
That’s simply because Rails is not aware of this class being declared here. We just have to tell him! To do that, we can use the autoload method.
Create an initializer in config/initializers named animal_loading.rb and put the following inside:
# config/initializers/animal_loading.rb
autoload :Lion, 'animal'
autoload :WildBoar, 'animal'
autoload :Meerkat, 'animal'
Thanks to this code, Rails will know where these classes are defined and they will be available anytime you need them!
2. Use a module
Another way to fix the subclasses loading is to use a module. This approach is far from the Rails way and I wouldn’t recommend using it. Anyway, let’s see how to do it.
What we’re going to do, is replace the animal.rb file that loads a class with a file that loads a module containing our classes.
First, let’s rename animal.rb to savanna.rb. Then, we have to change savanna.rb to look like this:
module Savanna
class Animal < ActiveRecord::Base
belongs_to :tribe
self.inheritance_column = :race
validates :race, presence: true
scope :lions, -> { where(race: 'Lion') }
scope :meerkats, -> { where(race: 'Meerkat') }
scope :wild_boars, -> { where(race: 'WildBoar') }
def talk
raise 'Abstract Method'
end
class << self
def races
%w(lion wild_boar meerkat)
end
end
end
class Lion < Animal; end
class Meerkat < Animal; end
class WildBoar < Animal; end
end
See what we did there? Now Rails will autoload the module Savanna which contains our parent class and its subclasses. Main inconvenient: you will have to add the module name before your subclasses when you use them:
Savanna:Animal
Savanna::Lion
Savanna::Meerkat
Savanna::WildBoar
If you don’t like to have a lot of files when using Single Table Inheritance, you can use these methods. However, I would still recommend splitting your subclasses in different files, especially if you plan to add some code inside them.