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?
1 Answer 1
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 => {}
).
-
\$\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 likedelegate_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– Steve K2021年07月11日 03:45:51 +00:00Commented 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\$Jörg W Mittag– Jörg W Mittag2021年07月11日 10:02:48 +00:00Commented 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– Steve K2021年07月12日 05:11:32 +00:00Commented 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– Steve K2021年07月12日 05:33:32 +00:00Commented 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
isnil
,empty
, etc? \$\endgroup\$Steve K– Steve K2021年07月12日 13:24:38 +00:00Commented Jul 12, 2021 at 13:24