Search code examples
ruby-on-railsrubyruby-on-rails-6.1

Initialize from within instance method call


In my Rails application. I have a module in which I am overriding .eql? method like below

# lib/item_util.rb

module ItemUtil
  def eql?(item, field: "cost", op: "==")
    item.send(field).present? &&
      item.send(field).send(op, self.send(field))
  end
end

Which is included in Item model

# app/models/item.rb

class Item < ApplicationRecord
  include ItemUtil
end

In my controller I want to check various conditions based on the attribute values. Ex:

@item1 = Item.find(:id)
@item2 = Item.find(:id)

@item1.eql?(@item2, field: "discount", op: ">") # @item2.discount > @item1.discount
@item2.eql?(@item1, op: "<=") # @item1.cost <= @item2.cost

# ...

All this is working fine and I want to write ItemUtil module in a neater way like below:

  module ItemUtil
    attr_accessor :item, :field

    def initialize(item, field: "cost")
      @item = item
      @field = field
    end

    def eql?(item, field: "cost", op: "==")
      new_item.present? && new_item.send(op, current_item)
    end

    def new_item
      @item.send(@field)
    end

    def current_item
      self.send(@field)
    end
  end

This returns TypeError (nil is not a symbol nor a string) for @field inside new_item method, as initialize wasn't invoked anywhere

Traceback (most recent call last):
        2: from lib/item_util.rb:12:in `eql?'
        1: from lib/item_util.rb:17:in `new_item'
TypeError (nil is not a symbol nor a string)

but I dont want to change the way I call .eql? on object i.e., I'd like to keep these lines intact

@item1.eql?(@item2, field: "discount", op: ">") # @item2.discount > @item1.discount
@item2.eql?(@item1, op: "<=") # @item1.cost <= @item2.cost
  • How do I get the new_item and current_item return the desired output?
  • How to invoke initialize method within .eql? method?
  • Is there an alternate approach for this?
  • Is there a way to access parameters in modules similar to before_action(in controllers)?

Solution

  • You cannot instantiate an instance of a module. (You can instantiate an instance of a class, but not a module.)

    In fact, what you are actually doing here is overriding the definition of Item#initialize!!

    Sticking with your general design pattern, what I would suggest is to abstract this logic into a new class - like, say, ItemComparator:

    class ItemComparator
      attr_reader :item, :other
    
      def initialize(item, other)
        @item = item
        @other = other
      end
    
      def eql?(field:, op:)
        item_field = item.send(field)
        other_field = other.send(:field)
    
        other_field.present? && item_field.send(op, other_field)
      end
    end
    
    module ItemUtil
      def eql?(other, field: "cost", op: "==")
        ItemComparator.new(self, other).eql?(field: field, op: op)
      end
    end
    
    class Item < ApplicationRecord
      include ItemUtil
    end