Identifying the objects in the Ruby way
Some time ago, I spotted a place for improvement regarding object identification in one of the applications I was working on. To give you the background - the application is a platform for companies, and each company can have its unique flow in the code, so we have to identify it somehow:
if company.id == 1245
do_something_extra
end
That kind of code is problematic for a few reasons:
- By looking at the code, you don’t know what company you are dealing with.
- Developers cannot memorize the ids of companies, each time, they have to look into the database when they want to modify the code with a unique flow.
- To test this code, you have to update or set the
id
attribute of theCompany
model; in the real world, we shouldn’t modify this attribute.
In addition to the previous code example, we also sometimes have the same flow for a few companies:
if [1234, 871, 7812].include?(company.id)
do_something_extra
end
We skip the standard flow for a given company:
if company.id != 1234
do_something_default
end
Or even for multiple companies:
if ![1234, 871, 7812].include?(company.id)
do_something_extra
end
It became evident that we have a perfect candidate for refactoring and we can reduce the application's technical debt.
Step 1: create mapping name - id
We started with creating a hardcoded mapping where the company name is the key, and the id is the value:
module Identification
COMPANY_MAPPING = {
"apple" => 1234,
"facebook" => 321,
"google" => 3471
}.freeze
end
With such mapping, we are sure that the identification won't be broken when someone would update the company name in the database. Also, with this solution, at least we can tell the company name:
if Identification::COMPANY_MAPPING['apple'] == company.id
do_something_extra
end
It seems that this solution is also problematic. To test it, we have to stub the constant.
Step 2: move the identification to the object
My second thought was that it should be possible to call the identification method on the object like this:
if company.is?(:apple)
do_someting_extra
end
Such a solution is readable and easy to stub. We had to turn the Identification
module into concern so we could easily include it inside the Company
model and define the #is?
method:
module Identification
COMPANY_MAPPING = { … }.freeze
def is?(company_name)
COMPANY_MAPPING[company_name.to_s] == id
end
end
The simple identification process was ready, but we have some cases left that are not yet supported. For example, sometimes we check if the company is not given a company or is in some companies' group.
Step 3: handling all cases in Ruby-way
If we want to check if the company is not given company, it would be great to call company.not?(:name)
:
def not?(company_name)
!is?(company_name)
end
If we want to check if the company is in the group of companies, it would be good to call company.one_of?(:apple, :google)
:
def one_of?(*company_names)
COMPANY_MAPPING.values_at(*company_names.map(&:to_s)).include?(id)
end
Having these methods defined, we can easily define the none_of?
method that will verify if the given company is not in the certain group:
def none_of?(*company_names)
!one_of?(*company_names)
end
Step 4: the complete solution
Finally, we ended up with a concern where the hash is defined along with four identification methods:
module Identification
COMPANY_MAPPING = { … }.freeze
def is?(company_name)
COMPANY_MAPPING[company_name.to_s] == id
end
def not?(company_name)
!is?(company_name)
end
def one_of?(*company_names)
COMPANY_MAPPING.values_at(*company_names.map(&:to_s)).include?(id)
end
def none_of?(*company_names)
!one_of?(*company_names)
end
end
and we can use it like this:
# Is given company?
company.is?(:apple)
# Is one of the companies?
company.one_of?(:apple, :google)
# None of the companies?
company.none_of?(:google, :facebook)
# Is not the given company?
company.not?(:apple)