Posted on: Written by: K-Sato
⚠️ This article was posted over 2 years ago. The information might be outdated. ⚠️

Table of Contents

Common Refactoring approaches

Extract Method

You move some code from an old method into a new method. This will allow you to have smaller methods with descriptive names.

Before

def report
  sold_items = %w[apples lemons grapes]
  puts "*** Sales Report for #{Time.now.strftime("%d/%m/%Y")} ***"
  sold_items.each { |i| puts i }
  puts '*** End Of The Report ***'
end

After

def report
  sold_items = %w[apples lemons grapes]
  puts "*** Sales Report for #{current_date} ***"
  print_items(sold_items)
  puts '*** End Of The Report ***'
end

def current_date
  Time.now.strftime("%d/%m/%Y")
end

def print_items(items)
  items.each { |i| puts i }
end

Refactoring Conditionals

You can also refactor complicated conditionals into methods to make them more readable.

Before

def check_working_hour
  if Time.now.hour >= 9 && Time.now.hour <= 17
    'You should be working.'
  end
end

After

def check_working_hour
  if working_hour?
    'You should be working.'
  end
end

def working_hour?
  Time.now.hour >= 3 && Time.now.hour <= 17
end

Inline Method

When a method body is more obvious than the method itself, jsut directory write the content of the method instead of creating an unecessary method.

Before

def result(score)
  if check(score)
    "Pass"
  end
end

def check(score)
  score > 70
end

After

def new_result(score)
  if score > 70
   "Pass"
  end
end

Extract variable

You move some code into a new variable for better readability.

Before

def yesterday
  (Time.now - 86400).strftime("%d/%m/%Y")
end

After

def new_yesterday
  one_day = 86400
  (Time.now - one_day).strftime("%d/%m/%Y")
end

Other refactoring approaches

Use default values

You can set a default value for an argument to make your code simpler.

Before

module TwoFer
  def self.two_fer(name = '')
    if name.empty?
      'One for you, one for me.'
    else
      "One for #{name}, one for me."
    end
  end
end

After

module TwoFer
  def self.two_fer(name = 'you')
    "One for #{name}, one for me."
  end
end

scan() to get the first letter of each word

With String#scan and a regex, you can get the first letter of each word. For instance, scan with the regex /\b\w will catch word boundaries (space, - and more) followed by a word character.

Before

def abbreviate(phrase)
  abbreviation_array = phrase.split(/[\s|-]/).map { |word| word[0] }
  abbreviation_array.flatten.join('').upcase
end

After

def abbreviate(phrase)
  phrase.scan(/\b\w/).join('').upcase
end

Use attr_reader to access instance variables

Why I always use attr_reader to access instance variables - ivo’s awfully random tech blog

Before

class Car
  def initialize(name)
    @name = name
  end

  def name
    @name
  end
end

After

class Car
  attr_reader :name

  def initialize(name)
    @name = name
  end
end

lines or each_line to split multiline strings

Using String#lines/each_line is more intention revealing.

Before

def rows(nums)
  nums.split(/\n/)
end

After

def rows(nums)
  nums.lines
end

chars over split

Using String#chars is more intention revealing.

Before

"test".split('')

After

"test".chars

each_with_object

Just read this article! Incredibly easy to understand how each_with_object works and what it is good for .

Usage

The most useful and I think the most popular usage of each_with_object is putting hash or array as an argument.
You don’t need to declare array or hash before your loops.

# Hash
%w[string1 string2 string3].each_with_object({}){
  |item, hash| hash[item] = item.upcase
}

# Array
(1..10).each_with_object([]) do |item, array|
  array << item ** 2
end

# Hash.each_with_object
{key: "value", key2: "value2"}.each_with_object({}) do |(key, value), hash|
  hash[value] = key
end
#=> {"value"=>:key, "value2"=>:key2}

Instance method in a class method

Before

class Foo
  def self.bar
    'BAR'
  end

  def bar
    'BAR'
  end
end

After

This class method is called a concenience method.

class Foo
  def self.bar
    Foo.new.bar
  end

  def bar
    'BAR'
  end
end

count all? any? none? and one?

Example


# Evaluate the number of different letters.
'aaa'.chars.zip('ass'.chars).count { |a, b| a != b }

# Check if all elements are 'a'.
'aaas'.chars.all? { |letter| letter == 'a' } #=> false

# Check if there is any 'a'.
'aaas'.chars.any? { |letter| letter == 'a' } #=> true

# Check if there is no 'a'.
'aaas'.chars.none? { |letter| letter == 'a' } #=> false

# Check if there is only one 'a'.
'aaas'.chars.one? { |letter| letter == 'a' } #=> false

scan to make an array

Before

'word'.chars.select { |letter| /\w/ === letter}

After

'word'.scan(/[a-z]/)

sum for cleaner caliculations

Before

POINTS_AND_LETTERS = {'A'=> 1, 'B'=> 2, 'C'=> 3}

def score
result = 0
'ABC'.strip.upcase.chars.each do |letter|
  result += POINTS_AND_LETTERS[letter]
end
result
end

p score #=> 6

After

POINTS_AND_LETTERS = {'A'=> 1, 'B'=> 2, 'C'=> 3}

def score
  result = 'ABC'.strip.upcase.chars.sum do |letter|
    POINTS_AND_LETTERS[letter]
  end
end

p score

Dynamically call and define methods

You can dinamically define methods using Module#define_method and call methods with Object#send

define_method

class Klass
  define_method :my_method do |arg|
    arg * 2
  end
end

Klass.new.my_method(2) #=> 4

send

class Klass
 def p_name(name)
  name
 end
end

# Dinamically call a method
Klass.new.send(:p_name, "test") #=> test

Practical Example

Before

class Klass
  def ken
    "Ken"
  end

  def jack
    "Jack"
  end
end

After

class Klass
  def self.define_name(name)
    define_method(name) do
      name.to_s
    end
  end

  define_name :ken
  define_name :jack
end

Klass.new.ken #=> ken
klass.new.jack #=> jack

Tap

Before

user = User.new
user.username = "kartik"
user.save!

After

user = User.new.tap do |u|
  u.username = "kartik"
  u.save!
end

Small Tips

super

The super method calls the parent class method.

class A
  def test
    p 'A'
  end
end

class B < A
  def test
    super
    p 'B'
  end
end

B.new.test #=> A B

Safe navigation operator

If you want to be safe and not risk a nil error, you would write something like the following:

if account && account.owner && account.owner.address
....
end

with the Safe navigation operator (&.) you can write it like the code below.

account&.owner&.address

References

About the author

I am a web-developer based somewhere on earth. I primarily code in TypeScript, Go and Ruby at work. React, RoR and Gin are my go-to Frameworks.