Posts Refactor an ~if/then~ Statement
Post
Cancel

Refactor an ~if/then~ Statement

In reviewing some code for my Rock, Paper, Scissors program, I noticed a recurring Ruby Reek smell, Utility Function that suggested I might want to refactor. Here are the steps I took for future reference.

The Starting Code

Here’s what some of the code I was working on looked like when I started (source):

Move Class

1
2
3
4
5
6
7
8
9
10
11
class Move
  attr_reader :name, :winning_moves

  def >(other_move)
    winning_moves.include?(other_move.name)
  end

  def to_s
    name
  end
end 

Rock Class

1
2
3
4
5
6
7
8
9
10
class Rock < Move
  def initialize
    @name = 'rock'
    @winning_moves = ['scissors', 'lizard']
  end

  def win_message(other)
    "Rock crushes #{other}."
  end
end

Scissors Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Scissors < Move
  def initialize
    @name = 'scissors'
    @winning_moves = ['paper', 'lizard']
  end

  def win_message(other)
    if other.name == 'paper'
      "Scissors cuts paper."
    else
      "Scissors decapitates lizard."
    end
  end
end

I had three other classes similar to the Scissors class that I’m not showing here.

This isn’t too bad as it is. The Rock class doesn’t raise any Rubocop or Ruby Reek warnings. The Scissors class and the three remaining classes, on the other hand, raised the Ruby Reek Utility Function smell for each win_message method.

1
Scissors#win_message doesn't depend on instance state (maybe move it to another class?) [UtilityFunction]

This basically means that the String returned from the if statement branches does not have any relationship to the rest of the class. That is, the string doesn’t depend on the state of the class or on anything else for that matter. It’s just a string with no connection to anything.

I think this is a sign that the code might make more sense in a different class or module.

Refactoring

I wasn’t sure what to do with this. I’ve seen this utility function smell many times before and at my current level, I often end up disabling the smell and ignoring it. So, I started making small changes in hopes that I might see a pattern that I hadn’t seen previously.

The First Change

The first change I made was to make the strings dependent on the moves:

1
2
3
4
5
6
7
def win_message(other)
  if other.name == 'paper'
    "#{name.capitalize} cuts #{other}."
  else
    "#{name.capitalize} decapitates #{other}."
  end
end

That worked and successfully stopped the utility function warning. However, it introduced another Ruby Reek smell, the Duplicate Method Call smell.

1
win_message calls 'name.capitalize' 2 times [DuplicateMethodCall]

The Second Change

I’m starting to see a pattern. I’m still not sure how I’m going to determine which verb (cuts and decapitates in this case) to use. Using a hash came to mind, so I tried that.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Scissors < Move
  attr_reader :win_messages

  def initialize
    @name = 'scissors'
    @winning_moves = ['paper', 'lizard']
    @win_messages = { 'paper' => 'Scissors cuts paper.',
                      'lizard' => 'Scissors decapitates lizard' }
  end

  def win_message(other)
    win_messages[other.name]
  end
end

Great! No more Ruby Reek warnings. This code does look a lot cleaner, I had to admit. However, there’s still some room for improvement and it’s at this point where I see an even broader pattern. What if I combine the two instance variables into one instance variable using the hash instead of the array?

1
2
3
4
5
6
7
8
9
10
11
class Scissors < Move
  def initialize
    @name = 'scissors'
    @winning_moves = { 'paper' => 'Scissors cuts paper.',
                       'lizard' => 'Scissors decapitates lizard' }
  end

  def win_message(other)
    winning_moves[other.name]
  end
end

That works, and it’s even cleaner.

The Third and Final Change

That’s when I realized that with some more tweaking, I could move the win_message instance method into the Move superclass, since at this point, it’s going to be the same code for each subclass. Yet, doing so, created the problem I had before where I would not know when to use which verb. To solve this, I realized that I didn’t need to store the entire sentence in the @winning_moves hash, only the verb itself.

At the end, my classes looked like this:

Move Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Move
  attr_reader :name, :winning_moves

  def >(other_move)
    winning_moves.key?(other_move.name)
  end

  def win_message(other)
    "#{self} #{winning_moves[other.name]} #{other}."
  end

  def to_s
    name.capitalize
  end
end

Rock Class

1
2
3
4
5
6
7
class Rock < Move
  def initialize
    @name = 'rock'
    @winning_moves = { 'scissors' => 'crushes',
                       'lizard' => 'crushes' }
  end
end

Scissors Class

1
2
3
4
5
6
7
class Scissors < Move
  def initialize
    @name = 'scissors'
    @winning_moves = { 'paper' => "cuts",
                       'lizard' => "decapitates" }
  end
end

So, since each subclass inherits the methods (behaviors) from the superclass, each instance of a subclass will use those methods on its unique state. In the >(other_move) method, I used key? instead of include? because even though both work, I think key? is more explicit in terms of what the code is looking for. By the time the win_message is called, a winner has been determined and the desired @winning_moves instance variable is available. I also made the default output for the to_s method capitalized since the move name needs to be capitalized whenever it’s used.

Summary

The code looks much cleaner now and I’m no longer relying on if/then statements within each subclass to determine which String to return. This also gives me more flexibility to make changes to the wording or changes to the verbs in the future.

In looking at each subclass, I feel like there may be some way to condense this further, like using a YAML file or something like that to store the names of the moves, the winning moves, and their verbs. Then, I can have a method that plugs those in as needed.

For now, what I have will work.

This post is licensed under CC BY 4.0 by the author.