Lesson 8 / 25

Modules, Mixins and Namespaces

Share behaviour with modules using include, extend and prepend, and organise code with namespaces.

Ruby's answer to multiple inheritance

A module is a collection of methods and constants that cannot be instantiated. Modules serve two purposes. As namespaces, they group related classes and avoid name clashes: Billing::Invoice versus Shipping::Invoice. As mixins, they share behaviour across unrelated classes. include adds a module's methods as instance methods (the module is inserted into the class's ancestor chain just above the class); extend adds them as singleton methods on an object or class (class-level methods); and prepend inserts the module before the class, so its methods run first and can call super to reach the class's own method, which is useful for wrapping behaviour such as logging or caching. Ruby's own Enumerable and Comparable are mixins: define each and include Enumerable to get map, select, sort and dozens more; define <=> and include Comparable to get comparison operators. ClassName.ancestors shows the method lookup chain, which explains which method actually runs.

Mixins in practice

Enumerable from one each method, Comparable from <=>, and prepend for wrapping.

module Billing
  class Money
    include Comparable
    attr_reader :paise

    def initialize(paise) = @paise = paise
    def <=>(other) = paise <=> other.paise          # gives <, >, ==, between?, clamp
    def to_s = format("Rs %.2f", paise / 100.0)
  end

  class Cart
    include Enumerable

    def initialize = @items = []
    def add(name, money) = (@items << [name, money]; self)
    def each(&block) = @items.each(&block)            # Enumerable needs only each
  end
end

module Audited
  def mark_paid!(*args)
    puts "audit: marking #{number} as paid"
    super                                            # bare super forwards args to Invoice#mark_paid!
  end
end
Invoice.prepend(Audited)

cart = Billing::Cart.new.add("pen", Billing::Money.new(4950)).add("ink", Billing::Money.new(19900))
puts cart.max_by { |_, m| m }.first                    # ink
puts cart.sort_by { |_, m| m }.map(&:first).inspect    # ["pen", "ink"]
puts Invoice.ancestors.first(3).inspect                 # [Audited, Invoice, Object]

Skills on a CV

A class inherits from one parent, like a family name, but can include many modules, like skills on a CV: "can be compared", "can be listed and sorted", "is audited". Any class can pick up the same skill.

Quick check: What do you need to define to gain Enumerable methods such as map and select in your class?

  • to_s
  • <=>
  • initialize only
  • each
Answer

each — Enumerable builds all its methods on top of each.