Concepts / Designing the Deck Class

Designing the Deck Class

A new Deck takes no constructor arguments and begins with 52 cards in a standard order, with every new deck identical to the others.

  • Programming

Guessing What a New Deck Holds

Imagine you are writing a card game and need a deck to play with. Before reading any method definitions, ask two simpler questions: what should a brand-new deck contain, and what should you have to tell the deck when you create it? Those two answers are the starting point for the specification of the Deck class, and they shape every method that follows. It is worth making your own guess first, because the specification is easier to follow once you know what problem it is solving.

What do you think happens?

Before reading on, what do you think a brand-new Deck contains, and does the caller need to supply anything to create it?

Reveal answer
Answer:

A new Deck takes no constructor arguments and begins with 52 cards in a standard order. Every new deck is identical to the others.

Because the starting state is fixed by the specification, the caller has nothing to choose. Creating a deck is simply a matter of asking for one.

Tracing a Deck From Creation

The Deck is a stateful object. It begins in a known condition, and each method either changes that condition or reports on it. The most useful way to understand the class is to follow its state from the moment it is created. A new deck starts with 52 cards, all in a standard order, and that order is the same for every new deck. From that point on, the deck's contents change only through the operations the specification allows: dealing a card removes one from the deck, and shuffling puts used cards back and rearranges them.

New deck52 cards, standard orderPartly dealtsome cards remainEmpty deck0 cards leftIllegalStateExceptionraised by dealCard()Full deckused cards returneddealCard()dealCard()last card dealtdealCard()shuffle()shuffle()
After each dealCard() call, what is the deck's remaining count, and what state is it in once cardsLeft() reaches zero?

Read the diagram as a set of states and transitions. The deck starts in the new state with 52 cards. Each dealCard() call moves it toward empty. Once the last card has been dealt, the deck is empty, and a further dealCard() call is an error. A shuffle() call returns used cards to the deck, so the deck can return to a full count from either the partly dealt state or the empty state. The diagram shows the card count and the reachable states, while the specification supplies the rules for each transition.

Why the Constructor Takes Nothing

The constructor builds the deck, and it takes no parameters. The specification defines the starting condition completely: 52 cards in a standard order, identical for every new deck. Because that condition is fixed, the caller has no decision to make when creating a deck. Supplying arguments would imply a choice the specification does not offer. Keeping the two starting conditions separate in your mind also prevents a common confusion. A new deck is in a standard order, which is the same every time. A shuffled deck is in a random order, which is not the same from one shuffle to the next.

The Three Operations on an Existing Deck

Once a deck exists, the specification gives it three operations: shuffle, dealCard, and cardsLeft. Each has a signature that tells a programmer what must be supplied and what comes back. Notice that only dealCard produces a value the caller must use. The other methods either change the deck's contents without returning anything, or return a count. This difference in return types is deliberate. It tells the caller what kind of information flows back from each operation.

Tracing a Deal Until the Deck Is Empty

Worked example

Dealing From a Full Deck

A program creates a new Deck, deals every card, and then calls dealCard() once more. What does the caller observe at each step?

  1. 1
    Create the deck

    The constructor takes no arguments. The deck starts with 52 cards in a standard order.

  2. 2
    Deal a card

    dealCard() returns a Card to the caller. The caller receives whatever card the deck decided was next, and the remaining count drops by one.

  3. 3
    Deal until none remain

    Each dealCard() call returns a Card. After the 52nd card is dealt, cardsLeft() reports zero.

  4. 4
    Deal from the empty deck

    The specification treats this call as an error. dealCard() throws an IllegalStateException, which is documented in the method's comment as the behavior when no more cards are left.

Result

The caller gets 52 Card values in turn. A 53rd call to dealCard() does not return a card. It throws an IllegalStateException.

The key point is where the behavior is specified. The fact that dealCard() throws an IllegalStateException on an empty deck is stated in the method's comment, which is part of the specification. A caller who reads the specification knows in advance that an empty deck is not a value to be handled quietly. It is an error the caller must avoid.

Guarding Against an Empty Deck

The surrounding program can avoid the error by checking the deck before dealing. The program asks cardsLeft() for the remaining count. If the count is greater than zero, it is safe to call dealCard(). If the count is zero, the program skips the deal. This check uses the deck's own answer, so it does not depend on a separate count kept by the program.

Call cardsLeft()remaining countCount above zero?Call dealCard()returns a CardSkip the dealno card requestedUse the Cardyesno
What path does the caller take when cardsLeft() is greater than zero versus zero, and where does dealCard() get called or skipped?

Mistakes When Using the Deck Contract

  • Common mistake

    Expecting a new deck to be shuffled

    What it looks like

    Assuming the first card of a freshly created deck is random.

    Why it fails

    A new deck begins in a standard order that is the same for every new deck. Randomness comes from shuffle().

    Do this instead

    Call shuffle() if you need a random order, and remember that the constructor gives a standard one.

  • Common mistake

    Expecting shuffle() to return the cards

    What it looks like

    Writing a value = deck.shuffle() and expecting a card or list back.

    Why it fails

    shuffle() returns nothing. The effect is seen only when the caller next asks cardsLeft() or deals a card.

    Do this instead

    Call shuffle() as a statement, then use cardsLeft() or dealCard() to observe the result.

  • Common mistake

    Dealing without checking for an empty deck

    What it looks like

    Calling dealCard() in a loop without ever asking cardsLeft().

    Why it fails

    dealCard() on an empty deck throws an IllegalStateException, as the method's comment specifies.

    Do this instead

    Check cardsLeft() is greater than zero before each dealCard() call.

  • Common mistake

    Keeping a separate count of dealt cards

    Why it fails

    A separate tally can drift out of step with the deck, and the specification already provides cardsLeft() for this purpose.

    Do this instead

    Use cardsLeft() as the single source of the remaining count.

Practice: Predicting a Deck's Behavior

MEDIUM

A program creates a new Deck and immediately calls cardsLeft(). Then it deals 52 cards and calls shuffle(). Then it calls cardsLeft() again. Predict the value returned by each cardsLeft() call, and state what dealCard() would do if called after the 52 deals but before the shuffle.

Hints
  • The constructor starts the deck with 52 cards.
  • shuffle() puts used cards back into the deck.
  • The specification says dealCard() throws an IllegalStateException when no cards are left.

Key Takeaways

  • A new Deck takes no constructor arguments and begins with 52 cards in a standard order, identical to every other new deck.
  • shuffle() returns nothing, puts used cards back into the deck, and rearranges the cards into a random order.
  • dealCard() takes no parameters and returns a Card. On an empty deck it throws an IllegalStateException, as stated in the method's comment.
  • cardsLeft() reports the remaining count, so the program can ask the deck rather than track deals itself, and can check it before dealing.