DEV Community

Cover image for TIL: Abstraction & refactoring with .send in Ruby
Daniel Sasse
Daniel Sasse

Posted on • Edited on

1 1

TIL: Abstraction & refactoring with .send in Ruby

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.

photo-1471188520444-a325e413e595

About DRY code:

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

The Setup

erd

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.

The Challenge

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:

def self.count_vhs_by_rentals
Rental.all.each_with_object({}) do |rental, vhs_hash|
vhs_hash[rental.vhs].nil? ? vhs_hash[rental.vhs] = 1 : vhs_hash[rental.vhs] += 1
end
end
#=> {<Vhs:0x00007f8d5e2bf0f0 id: 17, serial_number: "MAX-luqovgdk8e7tl", movie_id: 9 => 1,
#<Vhs:0x00007f8d4f81a4a0 id: 29, serial_number: "BABA-t6k24krzqclts", movie_id: 13 =>2,
#<Vhs:0x00007f8d4f854538 id: 11, serial_number: "BABA-begvxe7ai0rc6", movie_id: 13 =>3}
def self.count_vhs_by_client
Rental.all.each_with_object({}) do |rental, vhs_hash|
vhs_hash[rental.client].nil? ? vhs_hash[rental.client] = 1 : vhs_hash[rental.client] += 1
end
end
#=> {#<Client:0x00007f872c1bb4c0 id: 4, name: "Hank Jennings" =>1,
#<Client:0x00007f872c1c8fd0 id: 7, name: "Lisa Wilkes" =>2

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).

Implementing .send to call methods

The 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.

“hello”.capitalize #=> “Hello”
“hello”.send(“capitalize”) #=> “Hello”
view raw send_example.rb hosted with ❤ by GitHub

Abstracting & Refactoring the Aggregate Methods

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.

def self.count_vhs_rentals_by(attribute)
Rental.all.each_with_object({}) do |rental, vhs_hash|
vhs_hash[rental.send(“#{attribute}”)].nil? ? vhs_hash[rental.“#{attribute}”] = 1 : vhs_hash[rental.“#{attribute}”] += 1
end
end
Vhs.count_vhs_rentals_by(“vhs”) #=>
#{<Vhs:0x00007f8d5e2bf0f0 id: 17, serial_number: "MAX-luqovgdk8e7tl", movie_id: 9 => 1,
#<Vhs:0x00007f8d4f81a4a0 id: 29, serial_number: "BABA-t6k24krzqclts", movie_id: 13 =>2,
#<Vhs:0x00007f8d4f854538 id: 11, serial_number: "BABA-begvxe7ai0rc6", movie_id: 13 =>3}
Vhs.count_vhs_rentals_by(“client”) #=>
#{#<Client:0x00007f8d5e2ae6d8 id: 4, name: "Hank Jennings" =>1}
#<Client:0x00007f8d5e2be3a8 id: 7, name: "Lisa Wilkes" =>2}

Abstracting further — use by multiple classes

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.

def self.count_vhs_rentals_by(attribute)
Rental.all.each_with_object({}) do |rental, vhs_hash|
vhs_hash[rental.send(“#{attribute}”)].nil? ? vhs_hash[rental.“#{attribute}”] = 1 : vhs_hash[rental.“#{attribute}”] += 1
end
end
Vhs.count_vhs_rentals_by(“vhs”) #=>
#{<Vhs:0x00007f8d5e2bf0f0 id: 17, serial_number: "MAX-luqovgdk8e7tl", movie_id: 9 => 1,
#<Vhs:0x00007f8d4f81a4a0 id: 29, serial_number: "BABA-t6k24krzqclts", movie_id: 13 =>2,
#<Vhs:0x00007f8d4f854538 id: 11, serial_number: "BABA-begvxe7ai0rc6", movie_id: 13 =>3}
Vhs.count_vhs_rentals_by(“client”) #=>
#{#<Client:0x00007f8d5e2ae6d8 id: 4, name: "Hank Jennings" =>1}
#<Client:0x00007f8d5e2be3a8 id: 7, name: "Lisa Wilkes" =>2}

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.

module GenericHelpers
def make_hash_by_attribute(array, attribute)
array.each_with_object({}) do |array_item, new_hash|
new_hash[array_item.send("#{attribute}")].nil? ? new_hash[array_item.send("#{attribute}")] = 1 : new_hash[array_item.send("#{attribute}")] += 1
end
end
end

Accessing the Module from the Class

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).

  • The include keyword permits the class to use the methods from the specified module as instance methods in the class.
  • The extend keyword 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.

class Vhs
include GenericHelpers
extend GenericHelpers
def self.count_vhs_by_rentals
make_hash_by_attribute(Rental.all, "vhs")
end
def self.count_vhs_by_client
make_hash_by_attribute(Rental.all, "client")
end
end

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.

class Client
include GenericHelpers
extend GenericHelpers
def self.count_clients_by_age
make_hash_by_attribute(Client.all, "age")
end
end

Conclusion … More lessons learned

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…

Rental.group(:vhs).count #=>
#{<Vhs:0x00007f8d5e2bf0f0 id: 17, serial_number: "MAX-luqovgdk8e7tl", movie_id: 9 => 1,
#<Vhs:0x00007f8d4f81a4a0 id: 29, serial_number: "BABA-t6k24krzqclts", movie_id: 13 =>2,
#<Vhs:0x00007f8d4f854538 id: 11, serial_number: "BABA-begvxe7ai0rc6", movie_id: 13 =>3}

Lesson learned.

Read the blog on Medium

Image of Datadog

How to Diagram Your Cloud Architecture

Cloud architecture diagrams provide critical visibility into the resources in your environment and how they’re connected. In our latest eBook, AWS Solution Architects Jason Mimick and James Wenzel walk through best practices on how to build effective and professional diagrams.

Download the Free eBook

Top comments (0)

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay