Ruby code I no longer write

… and check why 5600+ Rails engineers read also this

Ruby code I no longer write

When we learn programming languages and techniques we go through certain phases:

  • Curiosity
  • Admiration
  • Overdosage
  • Rejection
  • Approval

etc. Similarly with other things we enjoy in our life such as ice cream, pizza and sunbathing :) We learn to enjoy them, we try too much of it and learn the consequences. Hopefully some time later we find a good balance. We know, how much of it, we can use without hurting ourselves.

I think we can have a similar experience in programming for example when you find out about metaprogramming, immutability, unit testing, DDD. Basically anything. We often need to hit an invisible wall and realize that we overdosed. It’s not easy at all to realize it and learn from it.

After 8 years of using Ruby and Rails, there are certain constructs that I try not to use anymore because I believe they make maintaining large applications harder.

Example 1

Here is a piece of code I wrote some time ago.

class Ticket
  def pdf_of_kind(kind)
    {
      normal: -> { self.normal_pdf },
      zebra:  -> { self.zebra_pdf  },
    }.fetch(kind).call
  end

  # normal_pdf and zebra_pdf methods defined somehow...
end

I am sure that, as most developers, you will find it… unusual. After all the code can be written as:

class Ticket
  def pdf_of_kind(kind)
    send(kind.to_s + "_pdf")
  end
end

However, there are certain problems with this construct.

It’s a bit more insecure. If kind comes from external sources we accidentally allow calling other methods which end in _pdf. Granted, you might think that in such case this should be prevented by different layer and perhaps you would be right.

But a much bigger issue for me is that this code is hard to refactor, hard to grep. If I try to find usages of zebra_pdf method before refactoring it, I won’t find out that pdf_of_kind is using it. If your codebase is small or you don’t have too many of such constructs, it doesn’t hurt much. But the larger the code is, the more you use it, the more you will find it is hard to change easily. Perhaps you read it as a sign that I miss static typing and you would be right. After so many years with Ruby, I miss the powerful refactoring tooling that comes with statically typed languages. Rubymine can do a lot, but there are limits to its features.

The surface of pdf_of_kind method is infinite. There is an infinite number of things it can do. I don’t handle infinite very well :). If you look more deeply at the code you will realize that it can do 3 things possibly. Run two methods or raise an exception in case of an incorrect argument (invalid kind). However, to find it out you need to look a bit more deeply. With the first implementation that I showed you, you quickly and easily see the limited scope of the function.

Example 2

Here is another similar example.

class Salesforce::Mapper
  private

  def parse_key(key_name)
    key_name.gsub('__c', '').underscore
  end

  def fill_keys_with_values(keys, data)
    hash = {}
    keys.each do |salesforce_key|
      our_key  = parse_key(salesforce_key)
      value = data[our_key]
      hash[salesforce_key] = value
    end
    hash
  end
end

class Salesforce::AccountMapper < Salesforce::Mapper
  def map(data)
    keys = %w(
      User_ID__c
      Phone
      Email__c
      IBAN__c
      CurrencyIsoCode
      Company_VAT_Number__c
    )
    fill_keys_with_values(keys, data)
  end
end

The purpose of this code is to map certain data computed in a report to salesforce columns that will be updated. Easy peasy.

However, again the number of columns that we will update is limited and predefined. There is no need for the mapping to be dynamic (computed based on gsub and underscore) instead of static (defined as A => B once). How would I write this code now:

class Salesforce::Mapper
  private

  def fill_keys_with_values(keys, data)
    hash = {}
    keys.each do |salesforce_key, our_key|
      hash[salesforce_key] = data[our_key]
    end
    hash
  end
end

class Salesforce::AccountMapper < Salesforce::Mapper
  def map(data)
    keys = {
      'User_ID__c' => 'user_id',
      'Phone' => 'phone',
      'Email__c' => 'email',
      'IBAN__c', => 'iban',
      'CurrencyIsoCode' => 'currency_iso_code',
      'Company_VAT_Number__c' => 'company_vat_number',
    )
    fill_keys_with_values(keys, data)
  end
end

Now the fill_keys_with_values method even seems unnecessary and could be refactored into using inject and the inheritance seems excessive.

If I ever want to find out where is currency_iso_code used, I will know about its usage in AccountMapper.

What makes this refactoring possible? The fact that we have a limited and predefined number of attributes that we need to map between. If the number was unlimited then dynamic transformation would be the only possible solution and the right one. But there is no need for it if you know upfront about all possibilities.

Summary

Similarly, I try to avoid ActiveSupport extensions to String such as constantize or underscore to find ruby classes or build CSS classes. I prefer explicit mapping from Abc::Xyz to abc_xyz or whatever. That way when I Remove Xyz class I can also find the mapping and remember to remove other parts of code related to what I am removing.

On one hand Rails conventions are convenient, but on the other hand, whenever I refactor something I need to search for Abc::WhateverXyz, WhateverXyz, whatever_xyz and probably, even more, variations to be sure that everything is going to work. The more dynamic and meta your code is, the more such cases you will have. So I try to limit those situations when the mapping is, in fact, predefined and limited.

There are even more conventions in Rails which make refactorings harder. For example the usage of instance variables when rendering views that we described in Fearless Refactoring: Rails Controllers.

You might also like