Search code examples
rubymethod-chaining

Is it possible to know when a Ruby class has finished chaining methods?


I'm trying to determine if it is possible in Ruby to determine when a chain of methods has finished being called.

Consider that I have a class which retrieves information from a database. I often just want to call an instance of the class to access all the included data, but in many situations I chain additional methods which apply filters etc. Currently the actual data retrieval is performed by a method which is always called last, like so:

all_data = data_retriever.run
filtered_data = data_retriever.condition({k1: v1}).run
filtered_data = data_retriever.condition({k1: v1}).condition({k2: v2}).run

This works fine, but I can't help but think that Ruby may have a way to do this without tacking a method like .run on the end of every usage. Is it possible for my class to "know" when I've finished chaining methods? Such as a hook that executes after all the chained methods are called?

class DataRetriever 
  @conditions = {}
  def initialize()
  end
 
  def condition(condition_hash)
    @conditions << condition_hash
  end

  #...

  def after_chains
    # run the data retrieval code using the contents of @conditions
  end
end

Solution

  • Is it possible for my class to "know" when I've finished chaining methods?

    No, chaining the methods is equivalent to calling them one after another. The receiver cannot distinguish the call style. And this is actually good, consider the following:

    retriever = data_retriever.condition({k1: v1})
    retriever.condition({k2: v2}) if something
    retriever.condition({k3: v3}) if something_else
    data = retriever.run
    

    In the above example, you probably wouldn't want the first line to already trigger the data retrieval.

    Ruby may have a way to do this without tacking a method like .run on the end of every usage

    Instead of run, you could use a more subtle entry point to trigger the execution, e.g. each which is Ruby's default enumeration method:

    data = data_retriever.condition({k1: v1})
    
    data.each do |entry|
      # ...
    end
    

    Under the hood: (there are various ways to implement this)

    class DataRetriever
      def run
        @data = # retrieve actual data
      end
    
      def each(&block)
        run unless @data
        
        @data.each(&block)
      end
    end
    

    Just make sure to invalidate @data when more conditions are added (or alternatively freeze the object after run).

    You could even call each from inspect: (you might want to abbreviate long data)

    class DataRetriever
      def inspect
        "#<DataRetriever data=#{each.to_a.inspect}>"
      end
    end
    

    In irb:

    filtered_data = DataRetriever.new.condition({k1: v1})
    #=> #<DataRetriever data=[1, 2, 3]>