Deck/Card/Player/Dealer/GameRules classes, shuffling and dealing as discrete testable methods, and GameRules as an injected strategy for supporting multiple card games.
Published September 23, 2026
enum Suit { HEARTS, DIAMONDS, CLUBS, SPADES }
enum Rank { TWO, THREE, /* ... */ KING, ACE }
class Card { Suit suit; Rank rank; }
class Deck {
private final List<Card> cards = new ArrayList<>();
Deck() { for (Suit s : Suit.values()) for (Rank r : Rank.values()) cards.add(new Card(s, r)); } // standard 52-card build
void shuffle() { Collections.shuffle(cards); }
Card dealOne() { return cards.remove(cards.size() - 1); } // deal from the 'top' (end of list)
}
class Player { String name; List<Card> hand = new ArrayList<>(); }
class Dealer {
Deck deck;
void dealHands(List<Player> players, int cardsPerPlayer) {
for (int i = 0; i < cardsPerPlayer; i++) {
for (Player p : players) p.hand.add(deck.dealOne());
}
}
}
Separating shuffle() and dealOne() into their own single-purpose methods (rather than one monolithic "setup the game" method) is what makes each independently testable: dealOne() can be unit-tested by asserting the deck shrinks by exactly one card and the returned card is no longer in the deck, with zero dependency on randomness — shuffle()'s randomness can be tested separately (e.g. asserting the deck still contains all 52 unique cards after shuffling, without asserting a specific order). Bundling shuffle-and-deal into one method would force every test of dealing logic to also account for shuffle's randomness, an avoidable coupling.
interface GameRules {
int cardsPerPlayer();
boolean isValidPlay(Card card, List<Card> currentHand, GameState state);
Player determineWinner(List<Player> players, GameState state);
}
class PokerRules implements GameRules { /* 5 (or 2, for Hold'em) cards per player, hand-ranking win logic */ }
class BlackjackRules implements GameRules { /* 2 cards per player, closest-to-21-without-busting win logic */ }
class CardGame {
private final Dealer dealer;
private final GameRules rules; // injected — CardGame has zero knowledge of poker vs blackjack specifics
void start(List<Player> players) {
dealer.dealHands(players, rules.cardsPerPlayer());
}
}
This is Strategy pattern applied to "what game is actually being played" — Deck, Card, Dealer, and CardGame's own orchestration logic are entirely game-agnostic; every game-specific rule (how many cards to deal, what counts as a valid play, how a winner is determined) lives behind the GameRules interface. Supporting a new card game means writing one new GameRules implementation, with zero changes to the shared framework — directly reinforcing the same pattern-recognition skill Strategy Pattern and Tic-Tac-Toe / Board Game Design's WinningStrategy build.
Q: Why deal from the end of the list (cards.remove(cards.size() - 1)) rather than the front? A: Removing from the end of an ArrayList is O(1) (no shifting of remaining elements); removing from the front is O(n) (every remaining card shifts down one position) — a minor but easy, free optimization once you're aware ArrayList's removal cost depends on position.
Q: How would isValidPlay() differ meaningfully between Poker and a game like Rummy? A: Poker's validity is mostly about betting/turn structure rather than card legality (any card in hand can generally be played per the betting round); Rummy's validity depends on forming actual valid melds/sequences from the hand — this difference is exactly what's isolated behind the GameRules interface, so CardGame's orchestration code never needs to know which validity model applies.
Q: Should Deck support more than one standard 52-card deck (e.g., games using multiple decks, or jokers)? A: Worth surfacing as a scoping question early (per the Object-Oriented Design Refresher's scoping guidance) — a small change to Deck's constructor (accepting a deck count, optionally including jokers as a configurable extra) accommodates this without restructuring the class, as long as it's anticipated rather than hard-coded to exactly 52 cards.
Q: Does GameState need to be its own class, or could game-specific state just live on Player/CardGame directly? A: A separate GameState class (holding things like the current pot in Poker, or the discard pile in Rummy) keeps game-specific mutable state cleanly separated from the game-agnostic Player/Dealer/Deck classes — bolting Poker-specific fields directly onto Player would leak game-specific concerns into a class meant to stay reusable across games.