19

I've put together two separate programs which play a card game called 'Crazy Eights'.

The classes I've written for this program are based on a default 'card' package which provides playing card objects and some generic methods for playing cards.

I've taken two separate approaches to achieve this which are both functional in their own right.

Here are two UML class diagrams which depict the two approaches:

Inherited subclass 'conversion' method

enter image description here

Composed subclass with similar methods

enter image description here

As you can see in approach 1 the class EightsCard contains a method convert(Card) Here's the method:

  /**
   * Converts a Card into an EightsCard
   * @param card The card to be converted
   * @return The converted EightsCard
   */
   public EightsCard convert(Card card) {
     if (card != null) {
     EightsCard result = new EightsCard(card.getRank(), card.getSuit());
     return result;
     } 
     return null;
   }
 }

This method allows you to call methods from CardCollection which otherwise wouldn't be legal. For example, in the play method from the EightsPlayer class shown below:

  /**
   * Removes and returns a legal card from the player's hand.
   */
  public EightsCard play(Eights eights, EightsCard prev) {
    EightsCard ecard = new EightsCard(0, 0);
    ecard = ecard.convert(searchForMatch(prev));
    if (ecard == null) {
      ecard = drawForMatch(eights, prev);
      return ecard;
    }
    return ecard;
  }

Approach 2 doesn't require any conversions as the similar methods have been written in a new class EightsCardCollection which extends CardCollection. Now the play methods can be written like this:

  public EightsCard play(Eights eights, EightsCard prev) {
    EightsCard card = searchForMatch(prev);
    if (card == null) {
      card = drawForMatch(eights, prev);
    }
    return card;
  }

This brings me to a couple of questions:

  • Are there any advantages to either approach beyond personal preference?
  • Is there a better way to compose this program?

For example, might it be better to write 'similar' classes which are more specific1 and not use default classes2 at all.

1 labelled 'crazyeights.syd.jjj' or 'chaptwelvetofort' in the class diagrams.

2 labelled 'defaults.syd.jjj' or cards.syd.jjj' in the class diagrams.

Rann Lifshitz
  • 4,040
  • 4
  • 22
  • 42
Josh Hardman
  • 721
  • 6
  • 17
  • @techtrainer I'm more interested in technical advantages in performance than opinion. If it's just a matter of personal preference then I don't really care either way. – Josh Hardman May 27 '18 at 07:22
  • 1
    I would have created a first version only containing the classes needed for 'Crazy Eights' and after that looked into refactoring the code but I would rather have extracted interfaces than super-classes. – Joakim Danielson May 27 '18 at 08:10
  • I've just starting reading about interfaces and abstract classes today (the original book I was working from didn't mention them). As you said @JoakimDanielson there seems to be sense in having the generic classes (Card, Card Collection, Player) as interfaces rather than super-classes. I have a lot to learn! – Josh Hardman May 28 '18 at 12:31

3 Answers3

10

Too many subclasses

Neither of these approaches is very good, as both of them have more subclasses than necessary. Why does EightsCard extend Card? What if you wanted to implement some other card game? (There are quite a few, you know...) Would you make one subclass for each card game? Please don't.

I would have these classes:

  • Card
  • CardCollection
  • Player

I wouldn't even have a Deck class as it seems like it does nothing else than extend CardCollection without providing any more functionality.

Then I would have one class for each game implementation, so one EightsController class that handles the game logic for that game. No specific class for EightsCard, no specific class for EightsCardCollection, etc.

The reason is simple: You don't need anything more than this. A Card and a CardCollection is exactly the same no matter which card game you are playing. A Player is also the same thing in all games.

Rann Lifshitz
  • 4,040
  • 4
  • 22
  • 42
Simon Forsberg
  • 13,086
  • 10
  • 64
  • 108
  • 1
    Side note: Once you have implemented this, I would be interested in reviewing your code at [codereview.se] – Simon Forsberg May 27 '18 at 13:01
  • EightsCard contains methods which could be awkward (and possibly static?) in a class other than itself or Card. As the methods in EightsCard are based solely on the game Crazy Eights I didn't want to include them in Card. For example, the cardMatches method checks whether two given cards match according to the specific rules of Crazy Eights. Also, Deck provides an extra constructor which constructs a standard deck of 52 cards. Wouldn't bundling all of the specific methods for a game in one class encourage awkwardly placed methods? Does your proposed structure have any performance benefits? – Josh Hardman May 28 '18 at 07:41
  • The Deck extending Card collection was taken as an example from a textbook. I'm not saying that makes it useful, they may have been illustrating that an inherited method can provide new constructors instead of recommending it as an appropriate solution. The thing that throws me off is that they were generally pretty clear about things like that throughout the book. Perhaps I'll write up another question on just that! – Josh Hardman May 28 '18 at 07:41
  • @JoshHardman It's a bit hard to comment on the exact details when I don't see your exact code - I only know the method headers and not the implementations. The cardMatches method can be placed in any class and just take two `Card` parameters and check if they are equal according to the Eights rules. So `public (static) boolean cardMatches(Card first, Card second)`. The Deck constructor that creates a deck of 52 cards would be better of as a static factory method instead of a constructor. You don't need to have all of this in *one* class, but you can have a different way of splitting it up. – Simon Forsberg May 28 '18 at 12:49
  • @JoshHardman My approach does not have much *performance* benefits - with code simple as this performance is not a concern. Possibly you would gain some performance and reduce some memory usage, but the most important aspect is the code structure which will give the code flexibility and maintainability so development time will be reduced. – Simon Forsberg May 28 '18 at 12:51
  • @JoshHardman I would really be interested in looking at your code if you post it on [codereview.se], then I can give you more concrete suggestions. – Simon Forsberg May 28 '18 at 12:51
  • I'll try and post it up soon. For now here's the original code I worked from: https://github.com/AllenDowney/ThinkJavaCode/tree/master/ch14 And actually this is practically the same as method 2: https://github.com/ApolloZhu/Think-Java-Exercises/tree/master/Chapter14 – Josh Hardman May 28 '18 at 12:54
  • @JoshHardman Do you happen to have your version on Github as well? – Simon Forsberg May 28 '18 at 12:58
  • I don't, nor do I have a Github account. I'll set one up and post up my version soon though. – Josh Hardman May 28 '18 at 13:11
  • 2
    @JoshHardman I would really recommend to post that for review at [codereview.se]. I promise to post a review of it if you do! – Simon Forsberg May 29 '18 at 15:31
  • [Here](https://codereview.stackexchange.com/questions/195462/simulations-of-the-card-game-crazy-eights) it is on Code Review – Josh Hardman May 30 '18 at 03:27
5

With regard to those domain models, I agree with Simon Forsberg that those are way too complicated, and I understand they're designed to demonstrate some concepts, but even then they could have used a more realistic example, like components in an automobile and their relations. You probably need only two domain classes, Card and Player. For an ordered collection of Cards, use a List<Card>. When it comes to dealing, taking turns, etc. there is a lot of room for creativity for how to organize game logic. Generally it is preferable to compose types out of other ones rather than rely heavily on inheritance which is less flexible in practice.

When it comes to performance, common rules apply. For instance, reuse objects where possible. If you're always using the 52-card deck then there's no reason to even have a Card class. There's probably nothing better-performing than reusing enumeration values everywhere.

enum Card {
    ACE_CLUBS,
    TWO_CLUBS,
    // ...
    ACE_DIAMONDS,
    // ...
}

(You can't really make this enum any more complicated, since different games use different orderings, place different values on different cards depending on the context, etc.)

For an excellent practical guide to programming for performance, I recommend the book, Java Performance: The Definitive Guide.

Lucas Ross
  • 1,049
  • 8
  • 17
1

Simon's comment caught my attention: "I would prefer having two enums - one for Suit and one for Rank - instead of one enum for the whole card"

Is there a better way to compose this program?

The key point is this -- most of your program should be completely insulated from the in memory representation of a card.

In memory, a card might be

  • an integer
  • a byte
  • an enum
  • a String
  • a URL (http://example.org/cards/hearts/7)
  • an integer/byte restricted to the range [0,52)
  • a tuple
    • int, int
    • enum, enum
    • byte, byte
    • mix and match
  • a reference to a third party implementation

Most of your code should not care.

Ideally, the only bits that care at all would be the module that provides the implementation, and perhaps the composition root.

Most places in your diagram where you have <<Java Class>> you should instead have <<Java Interface>>.

See Parnas, 1972

In that sort of a design, you would use composition to attach your crazy eight semantics to a generic definition of card defined somewhere else.

Eights.Deck deck(Iterable<Generic.Card> standardDeck) {
    List<Eights.Card> cards = new ArrayList();
    for(Generic.Card c : standardDeck) {
        cards.add(
            new Eights.Card(c)
        );
    }
    return new Eights.Deck(cards);
}

or perhaps

Eights.Deck deck(Generic.Deck standardDeck) {
    return new Eights.Deck(
         standardDeck
             .stream()
             .map(Eights.Deck::new)
             .collect(Collectors.toList())
    );
}

The composition root would look something like

public static void main(String [] args) {
    GenericCards genericCards = new ApacheGenericCards();
    EightsCards eightsCards = MyAwesomEightsCardsV2(genericCards);
    EightsGame game = new EightsGame(eightsCards)
    game.play();
}
Simon Forsberg
  • 13,086
  • 10
  • 64
  • 108
VoiceOfUnreason
  • 52,766
  • 5
  • 49
  • 91
  • I don't like that you are keeping both `EightsCard` and `GenericCards` - or are both of them interfaces? Or how do you glue all this together? – Simon Forsberg Jun 03 '18 at 19:54