The problem is defined as:
Given a list of hands dealt to two players, how many hands does Player 1 win?
DealHistory
is our root abstraction - a collection of Deal
s that has a single responsibility of counting wins of a specific player.
The abilities to get the total number of deals or to count wins of other players are not required to solve the problem at hand, but they make the class more generic and easier to test.
A Deal
represents Hand
s that different players hold in a single round.
We need to determine which hand wins, but we cannot do that in Deal
without exposing the internals of a Hand
, so let's delegate this responsibility.
A Hand
represents the five Card
s that a player holds.
Comparing two hands can produce three outcomes: first hand wins, second hand wins or both hands are of equal strength.
A Hand
implements java.lang.Comparable
to express this.
A Card
is defined by its value and suit.
As there are fixed sets of possible suits and values, let's model them as enumerations.
Let's add some tests before implementing hand ranking.
It is convenient to write tests using the same concise Hand
representation as in the provided input file. It also lets us reuse the same parsing logic.
We can consider adding Hand(String)
and Card(String)
constructors to perform the parsing there.
However, parsing strings is not the main concern of those classes.
On top of that, while representing a Card
as "2H"
seems pretty generic, representing a Hand
as "2H 3H 4H 5H 6H"
feels quite specific to the format of provided input file.
As this is a point of potential variability, let's better extract this logic into a separate HandParser
class.
Let's extract comparison of each rank into its own Comparator
and chain them together like this:
public int compareTo(final Hand other) {
return RoyalFlushComparator.INSTANCE
.thenComparing(StraightFlushComparator.INSTANCE)
.thenComparing(FourOfAKindComparator.INSTANCE)
.thenComparing(FullHouseComparator.INSTANCE)
.thenComparing(FlushComparator.INSTANCE)
.thenComparing(StraightComparator.INSTANCE)
.thenComparing(ThreeOfAKindComparator.INSTANCE)
.thenComparing(TwoPairsComparator.INSTANCE)
.thenComparing(OnePairComparator.INSTANCE)
.thenComparing(HighCardComparator.INSTANCE)
.compare(this, other);
}
Halfway through implementing it we see that some comparators are not isolated and actually depend on downstream comparators.
For example, if both hands have two pairs of equal values, TwoPairsComparator
only compares the pairs and then expects downstream HighCardComparator
to determine the winner.
However, the downstream OnePairComparator
then has to be aware of such a case and not try to determine the winner based on one arbitrarily chosen pair.
The dependence on downstream comparators could be removed without much trouble, but the code is also starting to show some smells:
- coupling of responsibilities (determining if current comparator is the one to decide the winner + doing the actual comparison)
- duplication
- use of
null
s to indicate lack of value
Let's look for a better approach.
As we have distinct ranks in a game of poker, we should have corresponding types in our model as well.
A curious case here is the Royal Flush. It could be both a separate type or just one case of a Straight Flush. But as we only care about ranking of hands and the same rules apply to both ranks, let's go with the latter approach for simplicity.
As we implement this approach, we see that the responsibilities are now better separated:
- determining the rank of a hand (
FiveCardHandFactory
) - comparing two hands (every
Hand
subtype)
Another benefit is that every Hand
subtype now holds just the data it needs, which makes individual comparison logic easier to understand.
If a card can only be in one hand at a time, hands like FullHouse
, ThreeOfAKind
or FourOfAKind
could store just one Value
.
However, while comparing the remaining cards (kickers) as well adds a small amount of code, it also gives us flexibility to support more poker variants. Let's assume we might need that down the road and keep this code.
With a quick implementation of input file parsing logic we verify that we get the expected answer.
As we revisit the quickly implemented TwoPlayerDealsSupplier
, we notice it has two responsibilities we can separate:
- reading a file and producing a list of lines (extract into a generic
ClasspathFileReader
) - producing
Deal
objects from these lines (keep inTwoPlayerDealsSupplier
)
First we separate the generic classes from those having anything to do with poker.
Then we follow the reader-friendly packaging guidelines:
- Packages should never depend on sub-packages
- Sub-packages should not introduce new concepts, just more details
The resulting structure is this:
- <root package>
- generic
- ClasspathFileReader
- Comparators
- pokerhands
- deal
- TwoPlayerDealsSupplier
- hand
- Card
- HandParser
- FiveCardHandFactory
- Deal
- DealHistory
- Hand
generic
package contains classes that could be reused in other projects or replaced by some general-purpose utility librarypokerhands
package contains classes representing the main concepts in the problem domain and subpackages encapsulating the secondary details
Let's take a closer look and apply "Don't create verb classes" advice:
By "Verb Classes", I mean classes whose names are really verbs in disguise. Things like Processor, Sender, Sorter, Listener. [...] To me, it seems obvious: if you have a class called "Sender", it's procedural code in disguise. What is being "sent"? Proper OO design would call for the thing being sent to be the class, and a "send" method on that class to do the actual sending.
Let's rename:
ClasspathFileReader
->ClasspathFile
TwoPlayersDealsSupplier
->TwoPlayersDealsFactory
HandParser
->HandFromStringFactory
If we look at our class names after this refactoring, we see that we have much less variety.
Essentially the only suffix we use is the -Factory
which produces objects of some type and the classes of objects themselves.