The other day after a good ol' code cleaning, the file I was working on seemed too long and repetitive. The reason to that was because of a set of four different methods doing the exact same thing: returning an argument.
More context:
The goal was to create an export of a collection.
For this I had two type of files:
application_exporter.rb
class ApplicationExporter
It has all the necessary methods to create a .xlsx file. It's also in charge of grabbing all the records for a given Model and apply to it a method to prepare these records (check the policy, apply filters, ...).
It looks like this:
def prepare_exported_data
@data = model_name
@data = apply_policy_to_data(@data, export_creator)
@data = apply_filters_to_data(@data, export)
@data = apply_scope_to_data(@data, export)
end
def model_name(name)
# takes the model name and returns Model.all in an array
end
def apply_policy_to_data(data, user)
# apply the policy to the data to make sure the user won't
# export data they shouldn't have access to
end
def apply_filters_to_data(data, export)
# if the user asked to see only a certain type of data,
# give them only what they asked
end
def apply_scope_to_data(data, export)
# in case there is a scope applied, let's apply it to the
# collection
end
model_exporter.rb
class ModelExporter < ApplicationExporter
This is the file for the Model records (it can be User, Posts,...). As the dependent of ApplicationExporter, it inherits from it all its methods.
This class will have all the necessary methods to fill the export, with the necessary attributes, and will define the columns, the headers, etc...
If I want something basic like exporting all the records of a given model, the only method it needs is the one defining the columns of the spreadsheet.
So what's the problem here?
My problem was that ApplicationExporter builds the export with a collection of records from the database by using the model_name
given to it when the process is fired. But this time the data came from a hash in the code base and only one attribute had to be fetched from the database.
That means many methods from ApplicationExporter were useless and I had to override them in the class ModelExporter.
First I had to create the collection of data I needed. For that I overrode the #model_name method:
def model_name
::VERY_IMPORTANT_HASH.map do |key, values|
{
name: key,
value: whatever_info_in_the_hash_for_this(key)
default_value: Model.find_by(name: key)&.default_value
}
end
end
Here, name and value are taken from the hash, but the default_value comes from the database. It returns an array (thanks to #map) which is great because this is the data type I need for the exporter process to work.
Now comes the moment when the method #prepare_exported_data
that I have shown above is fired, and also when the title of this post will finally make sense... (bear with me :) )
For this specific export I don't need any policies or filters applied, even if they exist. That means that every single method fired by #prepare_exported_data
has to return the collection without modifying it.
I ended up having to override all the methods this way:
def apply_policy_to_data(data, _user)
data
end
def apply_filters_to_data(data, _export)
data
end
def apply_scope_to_data(data, _export)
data
end
You can notice the underscores on all the arguments that doesn't concern the data. That means: "I know I am supposed to take 2 arguments, but I won't care about the second one <3".
Then, the only thing all the methods do is to return the data
passed as argument.
So how to avoid this 'repeat' mode? For that I used a concept from Calculus: Kestrel Combinator, also called K Combinator.
It is a function taking two arguments and returning only the first one, considered as a constant.
It is as simple as that!
Let's take a look at how to use it in my case:
def const(collection, _)
collection
end
alias_method :apply_policy_to_data, :const
alias_method :apply_filters_to_data, :const
alias_method :apply_scope_to_data, :const
There is nothing spectacular here: I achieved to have a cleaner and lighter code by using a little trick.
It is just one example about how to use a mathematic concept in code.
There are other kind of combinators, you can learn about them by taking a Calculus class... or reading this great article with Javascript examples. And if you had to use one of them, please share!
Top comments (0)