Development

Tutorial: Classes, Inheritance, Modules and Mixins in Ruby on Rails

By May 5, 2016 No Comments

In this post we will look at the patterns and tools available to us in Ruby on Rails for organizing our code. This is important for several reasons. Good organization helps make our code easier to read and easier to maintain. It also encourages separation of concerns and a scalable codebase, making future development more efficient. The Rails framework obviously provides us with a great deal of help with this upfront. Yet as our project grows larger, we may eventually find ourselves with dozens of models, controllers packed full of code and view logic overrunning our templates. It can be difficult to know how to proceed at this point, especially when operating within the monolithic Rails framework.

Let’s start by looking at some basic patterns in the Ruby language. Here’s an example of two classes following a basic inheritance pattern.

class Child < Parent

  attr_accessor :x

  def initialize(x, p)
    super(p)
    @x = x
  end

  def child_instance
    "child instance"
  end

  def self.child_class_method
    "child class method"
  end

end

class Parent

  attr_accessor :p

  def initialize(p)
    @p = p
  end

  def parent_instance
    puts "parent instance"
  end

  def self.parent_class_method
    puts "parent class method"
  end

end

Ruby also has a similar pattern known as the ‘mix-in’ pattern. In fact for many Ruby developers, mix-ins are actually preferred over class inheritance. In this method we define a module and then include it into our class. The module then imparts its functionality to the class which included it. Many different classes can include the same module and a single class can include many modules. The mixin pattern is a flexible and powerful means of sharing functionality.

Modules can seem a little strange at first. There are a few different ways one might use a Ruby module. You can use a module as a sort of self contained ‘library’ containing related functionality that can then be used elsewhere via ‘require’. Modules, unlike classes are not instantiated but can define methods on themselves via ‘self’. A module might be a good place to store complex/lengthy business logic. For instance let’s say you are writing a program that takes an uploaded file, parses it and returns some data. Your program is getting quite large so you decide to move the parsing functionality into a module.

module ParseFile
  def self.parse(file)
    return "ABCDEFG"
  end
end

Now in your main program you can just require your module and give it any file to parse. In Rails, we would probably put a module like this in the lib folder.

You can create large module ‘libraries’ consisting of many nested modules and classes. Here is a more complex example module used for processing data.

module DataProcessingModule

  def self.process_all_data(dataObj)
    a = DataProcessingOne.new(dataObj.slice(:keyA, :keyB))
    b = DataProcessingTwo.new(dataObj.slice(:keyC, :keyD))

    HelperModule.combine_data(a.process_data, b.process_data)
  end

  module HelperModule
    def self.combine_data(dataOne, dataTwo)
      return dataOne + dataTwo
    end
  end


  class DataProcessingOne
    def initialize(dataGroupOne)
      @data = dataGroupOne
    end

    def process_data
      puts "data A is #{@data}"
      return @data[:keyA] + @data[:keyB]
    end
  end

  class DataProcessingTwo
    def initialize(dataGroupTwo)
      @data = dataGroupTwo
    end

    def process_data
      puts "data B is #{@data}"
      return @data[:keyC] + @data[:keyD]
    end
  end

end

The module itself instantiates nested classes and makes use of the nested HelperModule. Note that this module also serves as a namespace for the two classes. After requiring the module in your program you could choose to just instantiate the class DataProcessingOne as follows:

DataProcessingModule::DataProcessingOne.new({keyA: 5, keyB 6})

In general modules can be used in this way to define simple namespaces which can be used to either encapsulate your objects/classes/data/functions etc. or as a means of grouping together related classes/functions/data etc. for organization purposes. Here is a module that is used to group three related classes together:

module M
  class A
  end

  class B
  end

  class C
  end
end

Class A is then accessible via M::A. Now it is clear that classes A, B, C are similar and that they are grouped together within module M.

Finally modules can be included in other classes to achieve the ‘mixin’ functionality first mentioned. Methods defined in a module can be either added to another Class’s class methods or instance methods. This is achieved via either ‘include’ or ‘extend’. Let’s define two modules and a class.

class MyClass
  include A
  extend B
end

module A
  def method_a
    puts "abc"
  end
end

module B
  def method_b
    puts "def"
  end
end

Now any instance of MyClass can call method_a. The class MyClass itself can now call method_b. Although not typically used in this way, modules can also instantiate new data/variables. To do this, it’s best to just define a distinct method in the module that will instantiate the data when called.

class MyClass
  include A
end

module A

  attr_accessor :var1
  attr_accessor :var2

  def initialize_module_data
    @var1 = "one"
    @var2 = "two"
  end
end

myclass = MyClass.new
myclass.initialize_module_data
myclass.var1
  --> "one"
myclass.var2
  --> "two"

Instead of using one module for extending class methods and another module for adding new instance methods, let’s create one module that does both. This will allow us to encapsulate more functionality.

class MyClass
  include A
end

module A

  def self.included base
    base.send :include, InstanceMethods
    base.send :extend, ClassMethods
  end

  module InstanceMethods

    def my_instance_method
      "module A instance method"
    end
  end

  module ClassMethods
    def my_class_method
      "module B class method"
    end
  end
end

When module A is included it calls the include and extend methods of the base object with either the InstanceMethods module or the ClassMethods module.

Classes, Inheritance, Modules and Mixins in Rails

Namespaces and Modules

Let’s start off simple by defining our Child and Parent classes from earlier in the models directory of our Rails project. Since these two classes are clearly related, let’s put them together in their own subdirectory which I will call test_space. Create the folder test_space within the models directory. We’ll then define our child class in ‘child.rb’ and our parent class in ‘parent.rb’.

class MyClass
  include A
end

module A

  def self.included base
    base.send :include, InstanceMethods
    base.send :extend, ClassMethods
  end

  module InstanceMethods

    def my_instance_method
      "module A instance method"
    end
  end

  module ClassMethods
    def my_class_method
      "module B class method"
    end
  end
end

Notice that since we moved our child and parent classes into the test_space folder we need to define our classes in the namespace TestSpace as per the rails convention. Now our parent and child classes are available to use throughout our Rails project and in the rails console. It might seem strange that we are defining normal ruby classes here instead of ‘actual’ ActiveRecord ‘models’ with associated tables. In actuality it can be very helpful to leverage ruby classes and modules for organization and separation of concerns throughout a Rails project. For now, since we are on the topic of ActiveRecord models let’s go ahead and create some. We’ll namespace our models in a new directory called ‘test_space_two’. Create a child model with the following command.

rails g model TestSpaceTwo::ChildModel name:string

You’ll notice that in addition to creating the model, Rails will create a new file in the models directory called test_space_two.rb. A little bit of Rails ‘magic’ is going on here. Let’s start by looking at the associated migration file.

class CreateTestSpaceTwoChildModels < ActiveRecord::Migration
  def change
    create_table :test_space_two_child_models do |t|
      t.string :name

      t.timestamps null: false
    end
  end
end

Notice that the table Rails created was named “test_space_two_child_models”. Rails is automatically matching the naming conventions of our tables to our namespace. Now let’s open up the file test_space_two.rb

module TestSpaceTwo
  def self.table_name_prefix
    'test_space_two_'
  end
end

Rails creates an actual module that matches our directory. ChildModel and every other model within the test_space_two folder becomes namespaced to this module. Furthermore by defining the table_name_prefix method Rails will ensure every table within this module will have the prefix “test_space_two”.

You may find it becoming tedious typing out TestSpaceTwo:: whenever you want to instantiate a model. Maybe all you want is just a nice subfolder to organize your models without needing to worry about namespaces. Rails does not load models from subfolders by default. By adding the following line to config/application.rb you can tell Rails to load models in a particular subfolder without needing to define a module/namespace.

config.autoload_paths += Dir[Rails.root.join('app', 'models', 'sub_folder_name')]

Or you can tell rails to load models in all model subfolders

config.autoload_paths += Dir[Rails.root.join('app', 'models', '{*/}')]

Mix-Ins and Concerns

Let’s use mix-ins to add some functionality to our ChildModel. In our test_space_two folder let’s create the file talk.rb and define the following module.

module Talk

  def self.included base
    base.send :include, InstanceMethods
    base.send :extend, ClassMethods
  end

  module InstanceMethods

    def say_name
      return self.name
    end
  end

  module ClassMethods
    def identify
      return "I am a talking class"
    end
  end
end

Now let’s include this module into ChildModel

class TestSpaceTwo::ChildModel
  include TestSpaceTwo::Talk

  #  name :string
end

Now our child model can say_name and identify. If desired we could use the Talk module on any model with a name attribute. It is also worth noting that all of our code is neatly organized together within the TestSpaceTwo directory/namespace.

In Rails we can push the mix-in functionality even further by defining validations and associations in our module. This allows for even more powerful levels of abstraction. For our module, anything that ‘talks’ should have a name so let’s add a name validation. Let’s also assume that anything that ‘talks’ has a teacher. (Note we have not actually defined the Teacher model here.)

module Talk

  def self.included base
    base.send :include, InstanceMethods
    base.send :extend, ClassMethods

    base.class_eval do
      validate :name, presence: true
      belongs_to :teacher
    end
  end

  module InstanceMethods

    def say_name
      return name
    end
  end

  module ClassMethods
    def identify
      return "I am a talking class"
    end
  end
end

Notice how the module adds the validation and rails association by evaluating those methods in the context of the base class.

You may find the above syntax to a bit unwieldy. Rails defines it’s own version of these mix-in modules known as ‘concerns’. You could move the above into the default ‘concerns’ folder located within ‘models’. The syntax for the above module would then be written as follows:

module Talk

  extend ActiveSupport::Concern

  included do
    validate :name, presence: true
    belongs_to :teacher
  end

  def say_name
    return name
  end

  class_methods do
    def identify
      return "I am a talking class"
    end
  end

end

This is a little cleaner and simpler to read and write. There is also a concerns folder within controllers. Concerns for the controller are used in mostly the same way as described here. A good place to start could be moving some of the methods in ApplicationController into a more compartmentalized concern. Remember when writing your controller concerns that rails instantiates a new controller object matching the route for every request. Concerns also have some other nice features that you can read about here:

http://api.rubyonrails.org/classes/ActiveSupport/Concern.html

Service Objects and Helpers

Service objects in Rails are objects that you instantiate whose main purpose is usually to handle some sort of complex intermediary logic. The difference between this and using a module is that since you are using an actual class you can instantiate your service object and define and manipulate your own state within this object. Service objects tend to be more closely related to a specific model whereas modules in the lib folder tend to be larger in scope, making use of models and modules across different areas of a project. For example, let’s suppose you have a document model. The document model has fields like name, date, category etc. The main part of the document model is the content field that is generated via some complex logic. Your document model is already packed full of validations, associations, scopes and other methods. We could move the logic for generating the content to a service object.

class MyNamespace::Document < ActiveRecord::Base

  before_save :generate_content

  def generate_content
    content_helper = MyNamespace::DocumentHelper.new(self.field1, self.field2, self.field3)
    content = content_helper.generate_content
    self.update_column(content, content)
  end
end

class MyNamespace::DocumentHelper
  def initialize(field1, field2, field3)
    @x = 1
    @field1 = field1
    @field2 = field2
    @field3 = field3
  end

  def generate_content
    # logic for generating content
  end
end

You should try to keep your controllers small. This means handling data and business logic at the model level whenever possible. For controllers it is probably best to either use concerns or inherit shared logic from another controller, perhaps ApplicationController. Similarly you should try to keep your views simple. To move logic out of your views you can either define helper methods in the corresponding controller or make use of Rails helpers. In the helpers folder, you can create a module following the Rails naming convention to match the corresponding controller. Methods defined in this module can then be used in the corresponding views. Rails creates corresponding helpers for you automatically when using the generate commands.

Inheritance in Rails, Abstract Class and Single Table Inheritance

We’ve already seen that we can use inheritance as normal with standard Ruby classes. We’ve also seen that inheritance is straightforward for our controllers. What about Rails models? Here there are two main options available to us. We can ‘inherit’ the parent model’s table via single table inheritance or we can inherit from the parent model in the usual way while keeping our own table via abstract class. In single table inheritance all of our models share the top level parent’s table. Define a ‘type’ column on the top level parent model and then Rails will automatically use this field to store and look up the correct class names of your models. This approach makes sense when the classes share much of the same data, otherwise your table will contain records with many nil values. In this case you might consider representing the relationship via a simple belongs_to association.

More information on Single Table Inheritance

The other option is to declare the parent class as an abstract class. This way you can still make use of the inheritance pattern while keeping your own table.

class ChildModel < ParentModel
end

class ParentModel < ActiveRecord::Base
  self.abstract_class = true
end

There’s no absolute rules for how to organize your code. Ruby coupled with Rails provides a great language that gives us a lot of options. Hopefully the patterns and tools discussed here can give you some ideas for when your project is starting to feel overwhelming or unwieldy.

Web Application Startup Guide

A 30-page ebook that covers positioning, marketing, pricing, and building your startup product, plus more.