Since reading Confident Ruby, I've been seeing the four distinct parts of my methods:
- collecting input
- performing work
- delivering output
- handling failures
Keeping these parts atomic makes the story strong, so I do my best to not unconsciously mix them up. Lately, I've been thinking about using adapters at the collecting input stage. This seems to make the work performance code more clear.
What's an Adapter?
The Adapater Design Pattern further separates interface from implementation, to the point where you can have a single interface for multiple implementations. Frameworks use adapters all the time. For example, ORMs like ActiveRecord. As the application developer, you'll use ActiveRecord's interface. But under the hoods, ActiveRecord uses the adapter pattern and can substitute in either Postgres, MySQL, or SQLite3 for the implementation.
While the pattern is useful for simplifying multiple implementations, it can be used to force a single interface too. For example, jQuery does a great job at this by always adapting matched elements to a collection. This happens regardless if you have zero, one, or multiple matched elements.
Here's what manipulating the DOM looks like in vanilla JavaScript:
// hiding one element
document.querySelector("#my-div").style.display = "none";
// hiding multiple elements
document.querySelectorAll(".my-div").forEach((div) => {
div.style.display = "none";
});
// verifying element exists to avoid null pointer exceptions
let div = document.querySelector("#maybe-div");
if (div) {
div.style.display = "none";
}
Here's what it looks like with jQuery:
// hiding one element
$("#my-div").hide();
// hiding multiple elements
$(".my-div").hide();
// no need to verify element exists, jQuery no-ops for no matched elements
$("#maybe-div").hide();
Of course there are drawbacks. Maybe we didn't want to silently fail for no matched elements. We also introduce extra complexity with the adapter implementation. You'll have to decide if the trade-off is worth it.
Examples for Method Parameters
On a smaller scale, we can use adapters for collecting input in our methods. This can open up our methods to a wide range of inputs, while keeping our implementation simple.
Consider this method, which allows for a single id or multiple ids as a parameter:
def perform_users(user_id_or_ids)
if user_id_or_ids.is_a?(Enumerable)
user_id_or_ids.each do |user_id|
# implementation here
end
else
user_id = user_id_or_ids
# repeated implementation here on single user id
end
end
Ruby has conversion functions, like Array()
, that are part of its standard library. These are all
idempotent. We'll use it to clean up the example above:
def perform_users(user_id_or_ids)
Array(user_id_or_ids).each do |user_id|
# implementation here
end
end
Some conversion functions you can use include: Array()
, Integer()
, Float()
, String()
,
URI()
.
In JavaScript, you can use constructors like Number()
and String()
to get the same effect. But
be careful with Array()
:
function performUsers(userIdOrIds) {
Array(userIdOrIds).forEach((userId) => {
// implementation here
});
}
performUsers("1"); // works as expected
performUsers(["1", "2"]); // wraps in a secondary array
performUsers(1); // does a no-op on an empty array
That's because the Array()
constructor isn't idempotent (it will wrap an Array in new one). Also
when called with a single integer argument, it will return an empty array using that argument as
the array's length.
In languages that support dynamic dispatch, like Crystal, it may be more clear to extract input conversions to its own method:
def perform_users(user_id : Int32)
perform_users([user_id])
end
def perform_users(user_ids : Array(Int32))
user_ids.each do |user_id|
# implementation here
end
end
Follow me via Newsletter, RSS feed, or Twitter.
You may also enjoy:
Chicago
·
Bishop, Zion, and Red Rocks
·
San Francisco
·
All Articles →