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 tomove2.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!
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
onmove1
andmove2
. 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
asksmove1
.The
wins_against?
method is deceptive and should really be namedis_beaten_by?
Thanks for reading & for your feedback 🙂
Yeah!! I noticed this!! Well done!!
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.
You are correct! I just mentioned this on the article, thanks for pointing it out.
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 returnstrue
because the logic is reversed. As noted by the update & other readers, flipping the order in that version would fix this.Thanks for reading 🙂
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 🙂
Isn’t this technique also known as “double dispatch”?
Yes, it is 🙂
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.
Thanks for your comment Dave! Do you have a code example you can share with us?
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)
That way if you want to add
:lizard
and:spock
you just put them inWIN_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
you could just do something around the lines
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.