0
\$\begingroup\$

I started with the following module:

Module DemoStructure
  @json_data = {...} # hash of json data, etc

  def self.website_data
    @json_data.map { |site_hash| hash.merge({ 'scope' => 'website' }) }
  end

  def self.find_field(source_data, key)
    data = source_data.flat_map { |tmp_data| tmp_data[key] }
    data.reject { |setting| setting.to_s.empty? }
  end

  def self.store_data
    find_field(website_data, 'stores')
  end
end

This implementation worked, but I thought it would be more readable if I was able to use find_field on the data it's searching rather than passing the data as an argument. So, I refactored the above to:

module DemoStructure
  def self.json
    @search ||= { data: {...} } # hash of json data, etc.
  end

  def self.result
    @result ||= { data: {} }
  end

  def self.find_field(field)
    result[:data].map { |setting| setting[field] }
  end

  def self.website_data
    result[:data] =
      json[:data].map { |site_hash| site_hash.merge({ 'scope' => 'website' }) }
    self
  end

  def self.store_data
    website_data.find_field('stores')
  end
end

Now DemoStructure.website_data.find_field('stores') works as expected, but DemoStructure.website_data doesn't because it returns self rather than the output of the map. So, I added

def self.output
  result[:data]
end

This works, but now I have inconsistency of usage between the website_data.output and store_data method.

In my mind, if I use website_data without chaining anything, I should get back the map of data as the method name implies. Otherwise, if it's part of a chain, it should return self.

Is this possible? (Some kind of "Is this method part of a chain" check?) Is it a good approach, or better to go back to the first implementation, find_field(data, field), which handles this use case but is subjectively less elegant?

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Not sure why you're using a module and not a class. If you want to group data and functionality together, you should use a class.

Is this possible? (Some kind of "Is this method part of a chain" check?) Is it a good approach, or better to go back to the first implementation, find_field(data, field), which handles this use case but is subjectively less elegant?

It's not really clear what you want to do with the result in case you don't chain it. However, I think you want to wrap it into an object and delegate to the data structure (the hash).

Something like this

class DemoStructure
  JSON = {} # not sure if this is static or where it's coming from?

  # we delegate all not implemented methods to @data
  # delegate_missing_to is implemented in ActiveSupport but
  # depending what you want to do with your result, there 
  # are other ways to implement this too
  # https://www.bigbinary.com/blog/rails-5-1-adds-delegate-missing-to
  attr_reader :data
  delegate_missing_to :data

  def initialize(data)
    @data = data
  end

  def self.website_data
    # We return a new DemoStructure object 
    new(JSON[:data].map { |site_hash| site_hash.merge({ 'scope' => 'website' }) })
  end

  def find_field(field)
    # return again a new DemoStructure object
    new(result[:data].map { |setting| setting[field] })
  end
end

# chaining of several find_field works
DemoStructure.website_data.find_field("foo").find_field("bar")
# We can call any method implemented on Hash because
# delegate_missing_to will delegate to it if it's not implemented
# in DemoStructure
DemoStructure.website_data.map { |el| el }

https://www.bigbinary.com/blog/rails-5-1-adds-delegate-missing-to

Edit

In Ruby, there are only three distinctions between classes and modules: only classes can inherit from classes, whereas both modules and classes can inherit from modules. You can only inherit from one class, but from many modules. Only classes can be directly instantiated. So, unless you want to directly instantiate the class, or you are forced to inherit from a class, you should use a module, since it gives both yourself and your clients more freedom.

I think this comment needs some clarification.

Modules provide a namespace

If you want to logically group things together into a namespace, you should use a module.

For example, you have search functionality in your app which you want to group together. This will avoid name clashes if you have another User or Product class.

module Search
  class User
  end

  class Product
  end
end

Search::User.new
Search::Product.new

Modules provide mixin facility

If you have functionality which you want to share between different classes you can use a module to mixin methods. I believe Jörg meant this functionality with inheriting from multiple modules. I wouldn't reference to this as inheritance in the traditional way though (like class Bar < Foo).

module Commentable
  def comment=(content); end
end

module Persistable
  def persist; end
end

class User
  include Commentable
  include Persistable
end

class Product
  include Commentable
  include Persistable
end

you should use a module, since it gives both yourself and your clients more freedom.

I definitely don't agree with this statement. Ruby is an object-oriented language and organising your code in classes where you group data and functionality together is a good practice. Most Ruby apps I worked with mostly use modules as namespaces or mixins and the majority of functionality is in classes.

I've seen Fowardable and SimpleDelegator so far, and from what I can tell, SimpleDelegator looks like it handles object delegation while Forwardable handles method delegation.

That's right, Fowardable and SimpleDelegator would both to the job here.

Here is an example using SimpleDelegator

class DemoStructure < SimpleDelegator
  def self.from_file(path)
    new(JSON.parse(File.read(path)))
  end

  def find_field(field)
    # return again a new DemoStructure object
    new(result[:data].map { |setting| setting[field] })
  end
end

One last question about this approach: What happens if any of the decorators in the chain return nil? Does each separate method need to check whether @data is nil, empty, etc?

You should always make sure that you have an object. If it's empty it shouldn't really matter. So in Ruby you can just always parse to an empty hash with to_h (e.g. nil.to_h => {}).

\$\endgroup\$
8
  • \$\begingroup\$ Thanks for this, it’s very helpful. To clarify, if I don’t chain, I want to return The resultant data structure at that point in the chain. In my example, This means returning data[:result]. Seems like delegate_missing_to allows for exactly that. The Json is static and comes from another class, parsed from a file. As for why I’m using a module instead of a class, to be honest, I wasn’t clear on when to use one over the other when I wrote this and I’ve since switched all of my modules over to classes. Any insight into the distinction would be welcome. \$\endgroup\$
    – Steve K
    Commented Jul 11, 2021 at 3:45
  • \$\begingroup\$ In Ruby, there are only three distinctions between classes and modules: only classes can inherit from classes, whereas both modules and classes can inherit from modules. You can only inherit from one class, but from many modules. Only classes can be directly instantiated. So, unless you want to directly instantiate the class, or you are forced to inherit from a class, you should use a module, since it gives both yourself and your clients more freedom. \$\endgroup\$ Commented Jul 11, 2021 at 10:02
  • \$\begingroup\$ Thanks @JörgWMittag! The project I'm working on deals mostly with singletons -- is there any best practice as to whether to use classes or modules in that case? \$\endgroup\$
    – Steve K
    Commented Jul 12, 2021 at 5:11
  • \$\begingroup\$ @Christian, in your code comments, you mention there are other ways to achieve delegate_missing_to which don't rely on ActiveSupport/Rails, etc. Would you mind elaborating on what's available in core ruby to achieve this to make your answer slightly more complete (since I'm not using AS or Rails)? I've seen Fowardable and SimpleDelegator so far, and from what I can tell, SimpleDelegator looks like it handles object delegation while Forwardable handles method delegation. \$\endgroup\$
    – Steve K
    Commented Jul 12, 2021 at 5:33
  • \$\begingroup\$ One last question about this approach: What happens if any of the decorators in the chain return nil? Does each separate method need to check whether @data is nil, empty, etc? \$\endgroup\$
    – Steve K
    Commented Jul 12, 2021 at 13:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.