Ruby mixins explained

Hi Devs,

In this post i will try to explain how to use ruby mixins for DRYing up your code. Let’s say I have Car which can drive and honk. It can find a car with vin number or return count.

class Car
  def self.find(vin)
    puts "return car with VIN number #{vin}"
  end

  def self.count
    puts "return number of cars"
  end
  def honk
    puts "I like to yell at people"
  end

  def drive
    puts "will try to avoid speed tickets"
  end
end

# A Car can drive, honk

car = Car.new
car.drive
car.honk

# Find car by its VIN number
Car.find("123")
# Get total number of cars
Car.count

Now we get a requirement to add Trucks to our inventory which do most of the things aCar does. Let’s start DRYing the code.

First I will extract instance methods using a module and use include to make them available. In ruby we use include to add instance level methods from a module.

module VehicleBehavior
  def honk
    puts "I like to yell at people"
  end

  def drive
    puts "will try to avoid speed tickets"
  end
end

class Car
  include VehicleBehavior

  def self.find(vin)
    puts "return car with VIN number #{vin}"
  end

  def self.count
    puts "return number of cars"
  end
end

Making progress. Now i will extract the class level methods into a module and use theextend keyword to make them available. In ruby we use extend to add class level methods from a module.

module VehicleBehavior
  def honk
    puts "I like to yell at people"
  end

  def drive
    puts "will try to avoid speed tickets"
  end
end

module VehicleData
  def self.find(vin)
    puts "return car with VIN number #{vin}"
  end

  def self.count
    puts "return number of cars"
  end
end

class Car
  include VehicleBehavior
  extend VehicleData
end

Good. But is there a way to combine both modules into one ? yes.

Every time a class includes module – Ruby will trigger the self.included method on that module. It will also pass class as a parameter. In our case the Car class will be the klass argument.

module Vehicle
  def self.included(klass)
    klass.extend(ClassMethods)
  end

  module ClassMethods
    def find(vin)
      puts "return car with VIN number #{vin}"
    end

    def count
      puts "return number of cars"
    end
  end

  def honk
    puts "I like to yell at people"
  end

  def drive
    puts "will try to avoid speed tickets"
  end
end

class Car
  include Vehicle
end

Looking good. But is there is a cleaner way to do this YesActiveSupport::Concern

require 'active_support/concern'

module Vehicle
  extend ActiveSupport::Concern

  def honk
    puts "I like to yell at people"
  end

  def drive
    puts "will try to avoid speed tickets"
  end

  class_methods do
    def find(vin)
      puts "return car with VIN number #{vin}"
    end

    def count
      puts "return number of cars"
    end
  end
end

class Car
  include Vehicle
end

car = Car.new

Car.find("blah")
car.drive
car.honk

ActiveSupport::Concern makes the syntax better and also has the advantage of  gracefully handling module dependencies.

Now our Truck class is as simple as this

class Truck
  include Vehicle
end

Pretty cool right? Less code! Less bugs!! YaY.