In my Indieweb Syndication code I was looking for a way to DRY-up two similar methods. I knew what I wanted to do, but wasn’t sure how to do it. I wanted to have some kind of template method (you’ll have to excuse me if my terminology is wrong) with placeholders for the parts where code diverged. I.e. I was thinking something along the lines of liquid or erb templates/placeholders for web pages but for actual code generation.
Here is a simplified example of what I started with:
def get(item)
succeeded = false
failed = false
until succeeded | failed
response = open("http://somewhere.com/?get=#{item}")
response_json = JSON.parse(response.string)
if (response.status[0] == "200") & response_json["posts"].any?
#Same code here for get and add
succeeded = true
elsif (response.status[0] == "200") & response_json["posts"].empty?
#Same code here for get and add
failed = true
end
end
succeeded
end
def add(item)
succeeded = false
failed = false
until succeeded | failed
response = open("http://somewhere.com/?add=#{item}")
response_json = JSON.parse(response.string)
if (response.status[0] == "200") & (response_json["result_code"] == "done")
#Same code here for get and add
succeeded = true
elsif (response.status[0] == "200") & (response_json["result_code"] == "item already exists")
#Same code here for get and add
failed = true
end
end
succeeded
end
You can see the two methods share an overall structure and also pieces of code within that structure. It would have been easy to write methods for the shared pieces of code within the conditionals, but that would have still left me with duplicated structures. After lots of reading around (it’s odd how all useful Ruby posts seem to have been written around 2006-2008) I came up with this idea:
{
"get" => {
"lambda_1" => lambda { |response_json| response_json["posts"].any? },
"lambda_2" => lambda { |response_json| response_json["posts"].empty? },
},
"add" => {
"lambda_1" => lambda { |response_json| (response_json["result_code"] == "done") },
"lambda_2" => lambda { |response_json| (response_json["result_code"] == "item already exists") },
}
}.each do |method, lambdas|
define_method(method) do |item|
succeeded = false
failed = false
until succeeded | failed
response = open("http://somewhere.com/?#{method}=#{item}")
response_json = JSON.parse(response.string)
if (response.status[0] == "200") & lambdas["lambda_1"].call(response_json)
#Same things here
succeeded = true
elsif (response.status[0] == "200") & lambdas["lambda_2"].call(response_json)
#Same things here
failed = true
end
end
succeeded
end
end
This uses a hash to hold the code differences for each method, via the use of a lambda
. This hash is then looped through and define_method
used to build the methods, with the appropriate lambda “replacing” the “placeholders”.
This is perhaps a bit messy, but I don’t mind it as it is just for me. I could tidy up the visual appearance of it somewhat just by not attaching the .each do
on the hash definition directly and instead using a variable.
There are pros and cons of this approach. The lambdas are a bit verbose. I.e I have to have lamba { |response_json| response_json["posts"].any? }
whereas if this was just a text placeholder it would only be response_json["posts"].any?
. Also, the placeholders aren’t true placeholders because you have to remember to call the required variable in the lambda. But, on the plus side there is no use of eval
and all code is proper code; it probably would have been possible to use liquid/erb to achieve the same end result, but I’m pretty sure I would have not been able to escape using eval
or having to define certain parts of code as text.
You almost certainly don’t want to write code like I do, but if you are interested here is the actual before and after code.