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 the Company 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)