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.
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
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?
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:
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
Isn’t this a lot cleaner? What do you think?
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
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.
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!