Modules, Roles, Mixins, and *ables in Ruby

Prerequisites

Demonstration

One of the most common problems that seem to naturally arise in code is handling objects that share a role. This tends to be an issue that is not immediately apparent, but as the application grows it begins to present itself. Let's illustrate this with a concrete example taken from the code used in this blog. A lot of this code will be ActiveRecord syntax, but the underlying concepts can be applied to plain old Ruby objects as well. When laying out out the code for the blog we have objects for Posts and BookReviews that look like the following:

class Post < ActiveRecord::Base
  belongs_to :author, class_name: 'User'

  validates_presence_of :title, :body, :author
  validates_length_of :title, maximum: 244
  validates_presence_of :author
  validates_presence_of :slug
  validates_uniqueness_of :slug, case_sensitive: false
  validates_length_of :slug, maximum: 244

  before_validation :parameterize_slug
  before_save :set_published_at_date

  def to_param
    slug
  end

  def self.published
    where(published: true)
  end

  def self.sorted_by_published_date
    order(published_at: :desc)
  end

  private

  def parameterize_slug
    self.slug = slug.to_s
    self.slug = slug.parameterize
  end

  def set_published_at_date
    if published_changed?(from: false, to: true)
      self.published_at = Date.today
    end
  end
end
class BookReview < ActiveRecord::Base
  belongs_to :author, class_name: 'User'

  validates_presence_of :book_title, :body, :book_author
  validates_length_of :book_title, :book_author, maximum: 244
  validates_presence_of :author
  validates_presence_of :slug
  validates_uniqueness_of :slug, case_sensitive: false
  validates_length_of :slug, maximum: 244

  before_validation :parameterize_slug
  before_save :set_published_at_date

  def to_param
    slug
  end

  def self.published
    where(published: true)
  end

  def self.sorted_by_published_date
    order(published_at: :desc)
  end

  private

  def parameterize_slug
    self.slug = slug.to_s
    self.slug = slug.parameterize
  end

  def set_published_at_date
    if published_changed?(from: false, to: true)
      self.published_at = Date.today
    end
  end
end

Let's go through this code a bit. You'll notice that despite being two different objects, they have quite a bit of behavior in common. To start, they both belong to an Author and validate that an author is present. Additionally they both have slugs for friendly urls, a call to the method #parameterize_slug to ensure the slug is URL friendly, and some validations to ensure the URL is present, unique, and it fits in the DB. Finally both of these objects can be marked as published and have scopes for filtering based on whether they're published or not.

Looking at this code we can see a lot of duplicate code. Both of these objects share similar roles and these roles have the same behavior. You could say they're both sluggable, publishable, and authorable. To refactor this duplicate code we should use Ruby's module. Ruby's module will allow us to 'mixin' common code to objects that share roles. Let's start with the code that makes an object play the authorable role.

module Authorable
  def self.included(base)
    base.class_eval do
      belongs_to :author, class_name: 'User'
      validates_presence_of :author
    end
  end
end

This is some dense code if you've never seen this pattern before, so let's step through it. The method .included is a built in hook method that gets called when a Ruby module is included in a class. The base argument is simply the included class. So by including the Authorable module into the Post class, the .included method will be called with the Post class set to base. Since base is now equal to Post we can open up the Post class can evaluate code using Ruby's class_eval method. The code inside the block will be evaluated as if it were in the Post class.

Note: This pattern is so common that the Rails framework actually provides a special module for this type of refactoring that's called a Concern. Feel free to use this module if you like, however I'm using the traditional way so we can use it without ActiveSupport.

Now that we lumped all the code related to making an object play the authorable role into the Authorable module, let's rewrite our Post and BookReview classes to use our new module.

class Post < ActiveRecord::Base
  include Authorable

  validates_presence_of :title, :body, :author
  validates_length_of :title, maximum: 244
  validates_presence_of :slug
  validates_uniqueness_of :slug, case_sensitive: false
  validates_length_of :slug, maximum: 244

  before_validation :parameterize_slug
  before_save :set_published_at_date

  def to_param
    slug
  end

  def self.published
    where(published: true)
  end

  def self.sorted_by_published_date
    order(published_at: :desc)
  end

  private

  def parameterize_slug
    self.slug = slug.to_s
    self.slug = slug.parameterize
  end

  def set_published_at_date
    if published_changed?(from: false, to: true)
      self.published_at = Date.today
    end
  end
end
class BookReview < ActiveRecord::Base
  include Authorable

  validates_presence_of :book_title, :body, :book_author
  validates_length_of :book_title, :book_author, maximum: 244
  validates_presence_of :slug
  validates_uniqueness_of :slug, case_sensitive: false
  validates_length_of :slug, maximum: 244

  before_validation :parameterize_slug
  before_save :set_published_at_date

  def to_param
    slug
  end

  def self.published
    where(published: true)
  end

  def self.sorted_by_published_date
    order(published_at: :desc)
  end

  private

  def parameterize_slug
    self.slug = slug.to_s
    self.slug = slug.parameterize
  end

  def set_published_at_date
    if published_changed?(from: false, to: true)
      self.published_at = Date.today
    end
  end
end

In the end we removed the duplication, but more importantly we learned a pattern that we can apply to the other roles these two objects have in common. Let's next tackle the common code that makes these objects publishable. Again, we'll start by creating a new Publishable module to put code for our publishable role.

module Publishable
  module ClassMethods
    def published
      where(published: true)
    end

    def sorted_by_published_date
      order(published_at: :desc)
    end
  end

  def self.included(base)
    base.extend(ClassMethods)
    base.class_eval do
      before_save :set_published_at_date
    end
  end

  def css_classes
    if published?
      return "published"
    else
      return "not-published"
    end
  end

  private

  def set_published_at_date
    if published_changed?(from: false, to: true)
      self.published_at = Date.today
    end
  end
end

You'll notice this module has a bit more to it than the Authorable module. Let's walk through the new changes. Right away you see that we have a module within the Publishable module called ClassMethods. We're going to use the ClassMethods module as a container for defining methods that we want to add as class methods (as opposed to instance methods) to our publishable objects. In our .included hook method that we used before, you'll notice that in addition to evaluating code we also extend our base class with the ClassMethods module. This adds (or mixes in) the methods in ClassMethods with the base or publishable class, but since we're using extend they're added as class methods, not instance methods. Lastly, we have the #css_classes and #set_published_at_date methods. These are instance methods and will automatically be included as instance level methods when the module is included. Once again let's refactor our Post and BookReview classes.

class Post < ActiveRecord::Base
  include Authorable
  include Publishable

  validates_presence_of :title, :body, :author
  validates_length_of :title, maximum: 244
  validates_presence_of :slug
  validates_uniqueness_of :slug, case_sensitive: false
  validates_length_of :slug, maximum: 244

  before_validation :parameterize_slug

  def to_param
    slug
  end

  private

  def parameterize_slug
    self.slug = slug.to_s
    self.slug = slug.parameterize
  end
end
class BookReview < ActiveRecord::Base
  include Authorable
  include Publishable

  validates_presence_of :book_title, :body, :book_author
  validates_length_of :book_title, :book_author, maximum: 244
  validates_presence_of :slug
  validates_uniqueness_of :slug, case_sensitive: false
  validates_length_of :slug, maximum: 244

  before_validation :parameterize_slug

  def to_param
    slug
  end

  private

  def parameterize_slug
    self.slug = slug.to_s
    self.slug = slug.parameterize
  end
end

Things are really starting to get cleaned up and duplicate lines of code are disappearing. Let's use the same pattern one last time for removing the duplication when it comes to the sluggable role. Once again we'll create a Sluggable module to correspond to the sluggable role and add the related code to the module.

module Sluggable
  def self.included(base)
    base.class_eval do
      validates_presence_of :slug
      validates_uniqueness_of :slug, case_sensitive: false
      validates_length_of :slug, maximum: 244

      before_validation :parameterize_slug
    end
  end

  def to_param
    slug
  end

  private

  def parameterize_slug
    self.slug = slug.to_s
    self.slug = slug.parameterize
  end
end

By now you are familiar with everything this module is doing. We are evaluating some code in the context of the included sluggable class within class_eval block and moving the instance methods relevant to the sluggable role into the module which will be added to the to the sluggable objects that include this module. Let's look at the final version of our Post and BookReview classes.

class Post < ActiveRecord::Base
  include Authorable
  include Publishable
  include Sluggable

  validates_presence_of :title, :body, :author
  validates_length_of :title, maximum: 244
end
class BookReview < ActiveRecord::Base
  include Authorable
  include Publishable
  include Sluggable

  validates_presence_of :book_title, :body, :book_author
  validates_length_of :book_title, :book_author, maximum: 244
end

Those classes are really compact now. We also now have the neat side effect that when we add other models to our blog, adding roles will be trivially easy. For example, if we decide we want to add a Tags model, all we have to do is include the functionality we want. Tags don't need to play the roles of authorable or publishable, but perhaps we want each Tag to have its own URL. To do this we can simply include the Sluggable module and be done with it (since this is technically a DB backed application we'll need to adjust the DB accordingly as well).

Warnings

While this pattern is powerful there are trade-offs we have to consider. When including modules, the functionality behind each role is now not as apparent. Someone who isn't familiar with your code might wonder where all the functionality is hidden. This also adds a bit of cognitive load for the programmer as they now have code for one object in 3 additional different places they must consider. When refactoring out into modules, or anytime when using inheritance, you'll want to make sure you're constantly asking yourself, will other programmers be able to easily navigate the code and find what they need.

We could, for example, create another module called Broadcastable that would look something like:

module Broadcastable
  include Authorable
  include Publishable
  include Sluggable
end

And then include this in our Post and BookReview classes. While this does DRY up the code a bit, I feel that it adds too much complexity and cognitive load. Its not quite as clear as to what Broadcastable does and adds another layer in the inheritance tree that programmers must process.

Another thing to be careful with is using this pattern simply to remove duplicate code. Its important to consider that the purpose of the duplicate code is the same as opposed to having duplicate code purely because of coincidence. If the purpose is aligned, then its much more likely the future changes will be compatible with all involved objects. If the duplicate code exists between objects coincidentally, then its much more likely that the duplicate code may diverge in the future. Sometimes you might even want to hold off refactoring until further down the line, when the true roles of objects become more clear.

With these warnings in mind, factoring out common code that objects share by virtue of having a role into a module is a great way to organize shared behavior.

See More:

To see how to test this pattern, you can view the source code for this blog. It makes use of rspec's Shared Examples. In minitest you would simply use modules!