I'm creating a tool to analyze a card game which is played by three players. A Card
is associated with a number of points and looks like this:
public enum Card
{
FIRST_CARD(1),
SECOND_CARD(2);
private final int points;
private Card(final int points)
{
this.points = points;
}
public int getPoints()
{
return points;
}
}
A game consists of multiple turns. Each turn each of the three players successivelys plays one card. These three cards go into something called a trick.
My question is how to model the trick in the best way.
Trick
s are stored in a List<Trick>
. The tool will take this List of Trick
s of an incomplete game (for example, 3 complete tricks and one trick for which only the first card has been played) and tries to find the optimal remaining plays for all of the three players; i.e. it will complete the List<Trick>
such that for all players the amount of points will be maxized. To be able to do that, it's necessary to have access to the three Card
s of the Trick
, to model incomplete Trick
s, to add Card
s to a Trick
and to get the points associated with a Trick
.
I wanted to go for an immutable data approach and came up with two options. The first option uses a regular class to model the different states a trick can be in (one, two or three cards). The second option combines sealed interfaces and multiple records; Brian Goetz calls this combination of sealed types and records algebraic data types in this article. This article also inspired me to try this second option.
First option
public final class Trick
{
private final Card firstCard;
private final Card secondCard;
private final Card thirdCard;
public Trick(final Card firstCard, final Card secondCard, final Card thirdCard)
{
this.firstCard = firstCard;
this.secondCard = secondCard;
this.thirdCard = thirdCard;
}
public Trick(final Card firstCard, final Card secondCard)
{
this.firstCard = firstCard;
this.secondCard = secondCard;
this.thirdCard = null;
}
public Trick(final Card firstCard)
{
this.firstCard = firstCard;
this.secondCard = null;
this.thirdCard = null;
}
public Trick withSecondCard(final Card secondCard)
{
return new Trick(firstCard, secondCard);
}
public Trick withThirdCard(final Card thirdCard)
{
return new Trick(firstCard, secondCard, thirdCard);
}
public Card getFirstCard()
{
return firstCard;
}
public Optional<Card> getSecondCard()
{
return Optional.of(secondCard);
}
public Optional<Card> getThirdCard()
{
return Optional.of(thirdCard);
}
public int getPoints()
{
var points = firstCard.getPoints();
if(secondCard!=null) points += secondCard.getPoints();
if(thirdCard!=null) points += thirdCard.getPoints();
return points;
}
}
Second option
sealed interface Trick {
public Card firstCard(); // Already implemented because of records' feature
public int getPoints();
record TrickWith1Card(Card firstCard) implements Trick
{
public Trick withSecondCard(Card secondCard)
{
return new TrickWith2Cards(firstCard, secondCard);
}
@Override
public int getPoints()
{
return firstCard.getPoints();
}
}
record TrickWith2Cards(Card firstCard, Card secondCard) implements Trick
{
public Trick withThirdCard(Card thirdCard)
{
return new TrickWith3Cards(firstCard, secondCard, thirdCard);
}
@Override
public int getPoints()
{
return firstCard.getPoints() + secondCard.getPoints();
}
}
record TrickWith3Cards(Card firstCard, Card secondCard, Card thirdCard) implements Trick {
@Override
public int getPoints()
{
return firstCard.getPoints() + secondCard.getPoints() + thirdCard.getPoints();
}
}
}
Which of these two options is better? These things come to my mind:
First option:
- Good: Uses only one class
- Bad: More boilderplate code (Constructors, methods for accessing the cards)
- Bad:
Optional
isn't that convenient to use for the client which access the cards of a trick
Second option:
- Bad: Having a
Trick
interface and three implementingTrickWith1Card
,TrickWith2Cards
andTrickWith3Cards
for the states a trick can be in feels a bit odd. However, maybe it only feels like that because algebraic data types are new in Java? - Good: Less boilerplate code
- Good: The client doesn't have to use
Optional
s. He can use pattern matching for switch with records to get the concrete record type and then access the cards
I'm wondering if using the interface approach for this use case is recommended. I'm not entirely happy about that the Trick
interface only has the firstCard
method. That's only the common denominator; shouldn't an interface describe what it's modelling (a Trick
) more exhaustively? What do you think?
Of course, if you have an alternative approach I would be interested in that as well!
PS: I intentionally omitted modelling the players because I don't think it's relevant for the core of question 'one class vs algebraic data types'.
comparative-review
tag to your question. \$\endgroup\$