14 comments
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?
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 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 🙂
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)
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.