ruby inheritance vs mixins

Viewed 37387

In Ruby, since you can include multiple mixins but only extend one class, it seems like mixins would be preferred over inheritance.

My question: if you're writing code which must be extended/included to be useful, why would you ever make it a class? Or put another way, why wouldn't you always make it a module?

I can only think of one reason why you'd want a class, and that is if you need to instantiate the class. In the case of ActiveRecord::Base, however, you never instantiate it directly. So shouldn't it have been a module instead?

7 Answers

The best way I understand mixins are as virtual classes. Mixins are "virtual classes" that have been injected in a class's or module's ancestor chain.

When we use "include" and pass it a module, it adds the module to the ancestor chain right before the class that we are inheriting from:

class Parent
end 

module M
end

class Child < Parent
  include M
end

Child.ancestors
 => [Child, M, Parent, Object ...

Every object in Ruby also has a singleton class. Methods added to this singleton class can be directly called on the object and so they act as "class" methods. When we use "extend" on an object and pass the object a module, we are adding the methods of the module to the singleton class of the object:

module M
  def m
    puts 'm'
  end
end

class Test
end

Test.extend M
Test.m

We can access the singleton class with the singleton_class method:

Test.singleton_class.ancestors
 => [#<Class:Test>, M, #<Class:Object>, ...

Ruby provides some hooks for modules when they are being mixed into classes/modules. included is a hook method provided by Ruby which gets called whenever you include a module in some module or class. Just like included, there is an associated extended hook for extend. It will be called when a module is extended by another module or class.

module M
  def self.included(target)
    puts "included into #{target}"
  end

  def self.extended(target)
    puts "extended into #{target}"
  end
end

class MyClass
  include M
end

class MyClass2
  extend M
end

This creates an interesting pattern that developers could use:

module M
  def self.included(target)
    target.send(:include, InstanceMethods)
    target.extend ClassMethods
    target.class_eval do
      a_class_method
    end
  end

  module InstanceMethods
    def an_instance_method
    end
  end

  module ClassMethods
    def a_class_method
      puts "a_class_method called"
    end
  end
end

class MyClass
  include M
  # a_class_method called
end

As you can see, this single module is adding instance methods, "class" methods, and acting directly on the target class (calling a_class_method() in this case).

ActiveSupport::Concern encapsulates this pattern. Here's the same module rewritten to use ActiveSupport::Concern:

module M
  extend ActiveSupport::Concern

  included do
    a_class_method
  end

  def an_instance_method
  end

  module ClassMethods
    def a_class_method
      puts "a_class_method called"
    end
  end
end
Related