Modules in Ruby

Currently, one hundred percent of Year of Commits' commits have been to Ruby repositories. Ruby has many strengths and is a very malleable language. Ruby can be written functionally or object oriented. I tend to lean toward the latter and find myself using a few different patterns regularly. One powerful tool Ruby provides is the use of Modules.

Modules

When code is to be shared between classes, a Module is created to encapsulate the shared functionality. A very simple module looks something like this:

module Ripe
  def ripe?
    puts "this is ripe!"
  end
end

extend

With the Ripe module, there are two ways to include its methods in a class. We can extend the module, which makes the method ripe? a class method on the Banana class:

class Banana
  extend Ripe
end

Banana.ripe?
# => this is ripe!

include

Alternatively, we can include the module, making ripe? an instance method:

class Banana
  include Ripe
end

banana = Banana.new
banana.ripe?
# => this is ripe!

That's great right? Right! We have methods on methods on methods. As long as we can make modules for our instance and class methods separately, we will be golden. But, what about all the times that we need to define both class and instance methods in one module? Surely it would be crazy to have RipeClassMethods and RipeInstanceMethods right? That looks like amateur hour. There must be a better way.

def self.included

A very helpful method for dealing with included modules is the included class method. This method is built in Ruby and any class has access to it. By using the included method, it is possible to mix class and instance methods within a single module.

When using self.included, we are able to determine which methods are accessible on the instance and on the class. We will use a new inner module named ClassMethods to encapsulate our desired class methods. In our example below, the single parameter base is the class in which the Ripe module is included.

module Ripe
  def self.included(base)
    # Just like in normal module extension,
    #  the extend method is used to make all methods in
    #  ClassMethods into proper class methods on base
    base.extend(ClassMethods)
  end

  module ClassMethods
    def ripe?
      puts 'this is ripe!'
    end
  end
end

Now, including the functionality from Ripe is the same as the include example above:

class Banana
  include Ripe
end

Banana.ripe?
# => this is ripe!

With this new structure, adding instance methods is as simple as adding them outside of the ClassMethods module.

module Ripe
  def self.included(base)
    base.extend(ClassMethods)
  end

  def color
    'yellow'
  end

  module ClassMethods
    def ripe?
      puts 'this is ripe!'
    end
  end
end

class Banana
  include Ripe
end

Banana.ripe?
# => this is ripe!

banana = Banana.new
banana.color
# => yellow

Boom, boom, pow, we have some nicely organized shared code. We are now able to easily define instance and class methods for our various important fruit related modules.