Search code examples
ruby-on-railsrubyincludeextend

Ruby extend & include tracing code


I'm confused about using "include" vs "extend, after searching for hours all I got is that module methods used with instance of the class including the module, and module methods used with the class itself when the class extending the module of those methods.

but this didn't help me to figure out, why this code give error when commenting the extend module line in "#extend Inventoryable" while work when uncomment it, here's the code

module Inventoryable

    def create(attributes)
      object = new(attributes)
      instances.push(object)
      return object
    end

    def instances
      @instances ||= []
    end

  def stock_count
    @stock_count ||= 0
  end

  def stock_count=(number)
    @stock_count = number
  end

  def in_stock?
    stock_count > 0
  end
end

class Shirt
  #extend Inventoryable
  include Inventoryable
  attr_accessor :attributes

  def initialize(attributes)
    @attributes = attributes
  end
end

 shirt1 = Shirt.create(name: "MTF", size: "L")
 shirt2 = Shirt.create(name: "MTF", size: "M")
 puts Shirt.instances.inspect

the output is

store2.rb:52:in `<main>': undefined method `create' for Shirt:Class (NoMethodError)

while when uncomment the "extend Inventoryable" to make the code work:

module Inventoryable

    def create(attributes)
      object = new(attributes)
      instances.push(object)
      return object
    end

    def instances
      @instances ||= []
    end

  def stock_count
    @stock_count ||= 0
  end

  def stock_count=(number)
    @stock_count = number
  end

  def in_stock?
    stock_count > 0
  end
end

class Shirt
  extend Inventoryable
  include Inventoryable
  attr_accessor :attributes

  def initialize(attributes)
    @attributes = attributes
  end
end

 shirt1 = Shirt.create(name: "MTF", size: "L")
 shirt2 = Shirt.create(name: "MTF", size: "M")
 puts Shirt.instances.inspect

makes the code work and output the following

[#<Shirt:0x0055792cb93890 @attributes={:name=>"MTF", :size=>"L"}>, #<Shirt:0x0055792cb937a0 @attributes={:name=>"MTF", :size=>"M"}>] 

it's kinda confusing, but all I need to know, is why I need to extend the module in order to avoid the error ?, and how to edit this code to make it work without the extend method ? , what's left in the code that still depends on the extend ?


Solution

  • When you extend a module, the methods in that module become "class methods"**. So, when you extend Inventoryable, create becomes available as a method on the Shirt class.

    When you include a module, the methods in that module become "instance methods"**. So, when you include Inventoryable, create is not available on the Shirt class (but is available on an instance of Shirt).

    To make create available on the Shirt class when using include, you can use the included hook. That might look something like:

    module Inventoryable
      module ClassMethods
    
        def create
          puts "create!"
        end
    
      end
    
      module InstanceMethods
    
      end
    
      def self.included(receiver)
        receiver.extend ClassMethods
        receiver.include InstanceMethods
      end
    end
    

    Then if you do:

    class Shirt
      include Invetoryable
    end
    

    You can do:

    > Shirt.create
    create!
     => nil 
    

    ** The ruby purists in the crowd will correctly point out that, in ruby, everything is an instance method and that there are no class methods. That is formally 100% correct, but we'll use the colloquial meaning of class and instance methods here.