A 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:
- Save time, by not re-writing similar logic again and again. Reduce the likelihood of typos leading to hours of painful debugging by reducing the number of times the same code is typed out.
- Make changes to code due to the implementation only occurring in one location.
- Rename or update variables or methods, without running the risk of forgetting to change the values in another place they were used and spend valuable time debugging a mysterious error.
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:
- Same method/function to be used in multiple places within the existing code.
- Extension of our design and allowing new features to be built off of the existing work
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:
- Genre has many movie_genres and movies through movie_genres,
- MovieGenre belongs to a movie and a genre,
- Movie has many movie_genres and genres through movie_genres,
- Movie has many vhs and rentals through vhs,
- Vhs belongs to a movie,
- Vhs has many rentals and clients through rentals,
- Rental belongs to a client and vhs,
- Client has many rentals and vhs through rentals.
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 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).
includekeyword permits the class to use the methods from the specified module as instance methods in the class.
extendkeyword permits the class to use the methods from the specified module as Class methods for the class
Both 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…