Stop Using Case Statements in Ruby

Are you using the full power of OOP (Object-Oriented Programming) or are you missing out?

If you are taking decisions based on the type of an object then you are missing out on one important OOP feature: polymorphism.

Type decisions are usually done inside case statements (which are not OO friendly) & in this article you will learn how to write better code by removing them.

Checking For Types

Let me start by showing you an example where we don’t take advantage of polymorphism.

We want to implement the “Rock, Paper, Scissors” game & we have decided to have one Game class and one class for every possible move.

To check for a winner we are going to implement a play method on Game:

class Game
  def self.play(move1, move2)
    return :tie if move1 == move2

    move1.wins_against?(move2)
  end
end

And here is one of the moves (the others follow the same pattern):

class Rock
  def wins_against?(other_move)
    case other_move
    when Paper then false
    when Scissors then true
    end
  end
end

Now we can call the play method with two moves & we will know if the first move wins.

p Game.play(Rock.new, Paper.new)
# false

Ok, this is working, but can we do better? Can we get rid of that ugly case statement?

Polymorphism Instead of Type Checking

Yes! We can get rid of the type-checking case statement by using OOP fundamentals.

The idea is to use the fact that we know the current class to ask the other movement object if it can beat us.

And we are going to use a method name that is specific to our class (for Rock the method name could be: do_you_beat_rock?)

In Ruby, polymorphism is the ability to send any method calls (also know as messages, in OOP parlance) to any object without having to check the object’s class. This is also known as “duck typing”, but I don’t like that term 🙂

In Java (bear with me for a second…), you have something called “interfaces” which allows you to force a set of methods to be implemented by a class at the compiler level.

We don’t have that in Ruby (probably for the better), so you will have to rely on testing.

Let’s see a code example of what this new implementation looks like:

class Rock
  def wins_against?(other_move)
    other_move.do_you_beat_rock?
  end

  def do_you_beat_paper?
    false
  end

  def do_you_beat_scissors?
    true
  end
end

Notice how the case statement is gone. It has been replaced by a single method call & two method definitions.

Update: As some readers have commented, I didn’t notice that the logic is reversed when implementing this pattern. One proposed solution by Daniel P. Clark is to just flip the order in the play method to move2.wins_against?(move1).

Isn’t this a lot cleaner? What do you think?

One More Move!

Now let’s say you want to add a new move. What would you have to change?

Think about it for a minute…

With the case statement approach you have to add a new branch to every move. Notice that you are “forced” to change a perfectly working method to introduce new functionality.

But that’s not an issue with our more OOP oriented version. All we have to do is add new methods. This is the open/closed principle (the “O” in SOLID) in action.

Another option could be to use metaprogramming (with method_missing or define_method).

Would I actually use metaprogramming for this?

Probably not, unless the number of moves is changing constantly, or there is a big number of moves.

There is a cost to metaprogramming, it’s not a silver bullet like some people may believe. You would be trading performance & readability for a bit of extra flexibility.

Conclusion

In this article you learned that you should avoid using case statements to check for class types. Instead, you should take advantage of polymorphism.

This will help you create better code that can be extended by adding new code instead of changing existing code. Now it’s your turn to take action & do some refactoring 🙂

Btw this doesn’t mean I’m advocating to entirely get rid case statements from your toolbox, but you should be wary whenever you find yourself writing one. Make sure that’s the best solution to the problem.

Don’t forget to share this article if you want me to keep writing articles like these!

14 thoughts on “Stop Using Case Statements in Ruby”

  1. Great post! And very efficient code. I implemented your version here: https://gist.github.com/danielpclark/63dbf528186c1e1d0fe0e8a65c39e6dc

    Two notes on Game.play.

    The :tie value won’t be returned because we haven’t defined == on the instances of each object. To make it work you need to call .class on move1 and move2. Second the method is actually a negative query.

    Rock asks Paper if it can win against it which means Rock needs the opposite answer to return. It works best if you flip the evaluation so that move2 asks move1.

    def self.play(move1, move2)
      return :tie if move1.class == move2.class
    
      move2.wins_against?(move1)
    end
    

    The wins_against? method is deceptive and should really be named is_beaten_by?

  2. Very nice article!!

    Just one thing. The logic seems not right.
    Because, you ask rock if wins against paper, then rock ask paper: do you beat rock?

    Example:
    Game asks: “rock.wins_against?(paper) you should get false”.
    But rock asks: “paper.do_you_beat_rock? the answer is true.”
    So your final answer is true, not false.

  3. In the current logic, when you do: result = Game.play(Rock.new, Paper.new), it returns true because the other_move “Paper” wins against Rock. Isn’t the result be false? In that case, self.play() should have move2.wins_against(move1).

    • Sorry for the confusion! In the case statement version (the first example) it returns false & in the polymorphic version (the second example) it returns true because the logic is reversed. As noted by the update & other readers, flipping the order in that version would fix this.

      Thanks for reading 🙂

  4. I think the idea is totally wrong ^_^

    Let’s add a new class Katana. So you have Rock, Paper, Scissors and Katana. How we add Katana logic in this approach?

    First of all we must implement “wins_against?” method. It’s a simple one. But also we must add a three methods for rock, paper, scissors checks (to satisfy duck typing).

    Also, we must add one method to each existing class (“do_you_beat_katana?”).

    Total: 3 + 3 + 1 = 7 one-liner methods.

    But when we use “case approach” we declare one method in Katana (which totally describes it behaviour) and add one-line when statements to each existing classes.

    Total: 1 complex method + 3 strings in other classes.

    7 items versus 4 items? It looks like your approach is over-OOP or over-SOLID solution.

    Moreover. Ruby isn’t OOP language. It’s hybrid language. Case against type is one of FP features. FP makes things more declarative and expressive. Of course “case approach” isn’t OOP at all. And shouldn’t be.

    • Thanks for your opinion. I don’t think the method count matters that much in this case, but like anything in code design there are no perfect answers 🙂

  5. Rather than bloating each class with a slew of methods, put the rules in a single class (could be based on type), basically a simple rules engine. Then when you add new types you change relatively simple logic in a single location, and a type can’t lie about what it beats.

  6. I agree with Dave here. Even though I understand what you mean in this post it seems that the example is not the best choice here.

    In general I feel that neither Rock nor Scissors or Paper should know who they win with. It’s the game that should know. Otherwise this knowledge is blurred amongst all of those objects and it is pretty hard to know in the end what the knowledge was. Instead I would keep this knowledge in 1 place (Game instance or whatever) so I could clearly see at once what it is, whether it’s correct or not. Moreover, if I need to add a new object to the game then instead of going to all of those classes and add appropriate methods I just add it in one place.

    Now the problem is how to keep a logic like that. You could do something like this (disclaimer – I wrote this code just for demonstration purposes so it’s very very so so :D)

    class Game
      WIN_TABLE = {
        rock: [:scissors],
        paper: [:rock],
        scissors: [:paper]
      }
    
      def self.play(move1, move2)
        return :tie if move1 == move2
    
        case
        when WIN_TABLE[move1].include?(move2) then :player1
        when WIN_TABLE[move2].include?(move1) then :player2
        else :unknown # or raise exception
        end
      end
    end
    

    That way if you want to add :lizard and :spock you just put them in WIN_TABLE with arrays of objects they win with (and add those objects to appropriate arrays of other objects – this sentence does not make any sense 🙂 )

    Of course, this representation of who wins with who is not perfect and it’s very easy to make a mistake. But you can either return :unknown in that case, or have a method that will verify that you covered all cases (shouldn’t be that hard to write).

    In the end – we have the rules of the game in 1 place and we clearly see who wins with who and so on.

    Now moving to the topic of case statements vs polymorphism then you may have another example that would seem more appropriate. Let’s say you have racing game and you have x cars that have their own turn to move. Each type of car has its own way of moving. Then instead of doing

    cars.each do |c|
      case c
      when Truck then c.position += 1
      when Van then c.position += 2
      when Formula1 c.position += 10
      end
    end
    

    you could just do something around the lines

    cars.each(&:move)
    

    and each car can define their own way of moving.

    Anyway, it’s pretty hard to figure out examples for this kind of things. But I guess it’s important to show proper stuff to people that come to our language (or start programming) so that they don’t get wrong ideas. Although, who is to judge which idea is wrong in the end. It’s all tradeoffs anyway 🙂

    What are your thoughts?

    Sorry for such long response. Good job with the blog, keep it up.

Comments are closed.