Published: Wed Dec 30 2020 (~ 5 min)
Read on Dev.toA beginner’s journey to writing DRY, modular, and extensible code in Ruby with ActiveRecord
Imagine we are building an app that represents an old-school VHS store. This was a task I paired on recently and we challenged ourselves to keep our code DRY.
One of the issues that new programmers struggle with is the DRY (Don’t Repeat Yourself) principle, which requires us to write maintainable and well-designed code.
Keeping code DRY allows us to:
Reduce the likelihood of typos leading to hours of painful debugging by reducing the number of times the same code is typed out.
One of the ways we learned to DRY our code was to abstract it and make the functionality less literal. As such this allow for the:
Since I will be referring to the models of the domain, the ERD for our VHS Store app can be seen above. The domain consists of six models (Genre, MovieGenre, Movie, Vhs, Rental, and Client) associated as follows:
The app utilized ActiveRecord for database management and associations, along with numerous CRUD (Create, Read, Update, Delete) methods that would allow the app users to manage the data and gather insights.
One of the functionalities to be added was finding the number of movies in each genre, the number of times each client rented a movie, or the number of VHS tapes owned for each movie title. This is the method we initially came up with:
The problem with this solution, is that after doing this similar implementation for just two (of the many) it was clear that we should DRY the code. But how do you abstract a code that requires different methods to be called depending on the attributes needed? Enter the Ruby .send
method. Akpojotor Shemi discusses the basics of .send
in their blog post Send Me a River (Ruby Send Method).
.send
to call methodsThe Ruby .send
method takes an argument (which can be a string or a symbol), and essentially calls the content of the argument as a method on the same instance that .send
was called upon.
This opens up a high degree of freedom and abstraction since method names can be passed as arguments to other methods as strings and utilized freely, whereas otherwise conditional statements or custom methods would need to be written each time. Using .send
, we could refactor our rental aggregation method as shown below. With the code being written in this format, we can now use one method to perform the work of both of the original methods by passing the the desired attribute as a string.
Now, what happens when we need to use this same logic in a different class? It wouldn’t make sense to rewrite the method yet again, instead, we should just abstract our method, one level further, to allow us to write the method into a Module and be included into the various classes so that we can use the same method between different classes. Second, if we look at the core of the original method (below), we are iterating through an array, counting the instances of each array item, and returning it as a hash.
Instead of hard-coding the array, we can just pass that in as a new argument. Note also that this refactored method is in the new module as well.
In our original VHS class, we need to remember to include/extend our class so that the shared methods can be called (See Mehdi Farsi’s blog post Modules in Ruby: Part I).
include
keyword permits the class to use the methods from the specified module as instance methods in the class.extend
keyword permits the class to use the methods from the specified module as Class methods for the classBoth of the keywords are used in this refactored example so that the helper method can be used in any scenario. With access to the module set up, we then just need to pass the array that we need to iterate through to the abstracted method.
Since the method is abstracted to be used with different arrays and different classes, we can use it with the same functionality in a new class, such as if we wanted to get a breakdown of the age demographic of our client base.
By abstracting our code using the .send
method, my partner and I made our code shorter, flexible, open for extension, more easily debugged, and more easily maintained across our Ruby app and allows for the creation of generic aggregate methods that may not be already built into Ruby.
As a note to my future self, remember to search the documentation for all of the tools at your disposal. While the abstraction implemented for this practice worked wonderfully and provided valuable lessons in refactoring, we afterwards discovered that ActiveRecord is even more magical than Ruby. ActiveRecord’s own set of methods easily accomplish our manually refactored method with its own innate set of tools…
Lesson learned.