In Progress
Unit 1, Lesson 1
In Progress

# Contextual Identity Part 2

Whether modeling cards in a game or shares in a corporation, sometimes you need to create objects which must exist only in a single collection at a time. In this concluding episode of a two-part series, we’ll introduce a new solution to the modeling problem posed in Part 1. You’ll see how the choice of whether to model a domain concept as an immutable value object needs to take into account not just the concept’s intrinsic properties, but also the context that it exists within.

Video transcript & code

In the last episode, we started talking about modeling card games in an object-oriented way. And we talked about how a "Card" object seems like a perfect example of a "value object": an immutable object which is defined solely in terms of its attribute. In this case, those attributes are Rank and Suit. One queen of hearts is interchangeable with another queen of hearts; these two cards have no unique identity.

But then we talked about how when we're working with value objects, it's really easy to accidentally introduce duplicates. For instance, we had this code that deals out 5 cards to hand… but winds up with duplicates of the card in the deck and in the hand.

``````hand = deck.take(5)
# => [Card(7 of Diamonds), Card(King of Hearts), Card(3 of Diamonds), Card(10 of Spades), Card(7 of Clubs)]

(deck + hand).size
# => 57
``````

In some applications, this sort of thing is perfectly fine and normal. In fact, it's exactly how we expect to use value objects. But in a card game, if we start introducing duplicate cards into a game, we have a serious problem.

This fact introduces an interesting nuance into the question of Value Object versus Entity. Intrinsically, these cards still have just the two attributes: rank, and suit. But their relationship with the game in progress has kind of an identity-like flavor to it.

And this is where we get into one of those subtleties of object-oriented domain modeling which is all too often glossed over in introductions to the topic.

Here's the thing: yes, in the abstract, we can say that a playing card is defined solely in terms of its rank and its suit.

But we're not modeling the platonic concept of a playing card. We're modeling a card game. And in the context of a single game, each card does have an identity. A contextual identity. There are 52 cards, and each one of them can be in just one stack or hand at any given time.

Let's try out an alternative modeling that recognizes this identity.

We'll define a new, improved `Card` object that has five attributes:

• `suit`
• `rank`
• A particular named `stack` of cards that it belongs to.
• An `ordinal` position in that stack.

We also define a convenience method for getting at the card's `deck` via its `stack`

…and a customized inspect method to make it display more readably.

``````Card = Struct.new(:suit, :rank, :stack, :ordinal) do
RANK_NAMES = [nil, "Ace", *(2..10), "Jack", "Queen", "King"]

def deck
stack.deck
end

def inspect
"Card(#{RANK_NAMES[rank]} of #{suit})"
end
end
``````

We'll set up a `Deck` class to contain these cards.

For now, we'll just hard-code an initializer that sets up a standard 52-card deck. In the process, it sets each card's initial stack to be the `:draw_pile`. We'll define this `stack` method in a moment.

It also shuffles the deck by taking the original indexes of all the cards, randomizing them, and assigning the randomized numbers as ordinals.

We create an accessor method for getting at a named `stack` of cards in the form of an object. We haven't defined the `CardStack` class yet; we'll get to that next.

We also define a method to return just the cards corresponding to a given stack.

``````class Deck
def initialize
@cards = %w[Hearts Spades Clubs Diamonds]
.product((1..13).to_a)
.map{|(rank,suit)|
Card[rank,suit,stack(:draw_pile)]
}
@cards.zip((0..@cards.size).to_a.shuffle).each do |card, ordinal|
card.ordinal = ordinal
end
end

def stack(stack_name)
CardStack[self, stack_name]
end

def cards_in_stack(stack)
@cards.select{|c| c.stack == stack}.sort_by(&:ordinal)
end
end
``````

We'll use the `values` gem to quickly define the `CardStack` class as a Struct-style immutable value object class.

The `CardStack` class represents… well, a stack of cards. But unlike the arrays we worked in the earlier episode, a `CardStack` won't "contain" cards in any real sense of the word. Instead, it will behave more like a handle to collections which exist inside the overall deck. As such, it has a reference to the deck, and an attribute for the name of the stack.

Most importantly, it has an each method that can iterate through the stacked cards in order.

…and it includes Enumerable to make full use of that each method.

As a convenience for moving cards into a stack, we provide a `next_ordinal` method to returns one more than the highest current ordinal in the stack.

Two more little tweaks before we're done: first, we enable card stacks to be created with struct-style subscripting by aliasing new to the square bracket operator.

And we redefine the to_a method to override the default `values` gem behavior. Instead, our version will return the list of cards.

``````require "values"

CardStack = Value.new(:deck, :name) do
include Enumerable

class <<self
alias [] new
end

def each(&block)
deck.cards_in_stack(self).each(&block)
end

def to_a
deck.cards_in_stack(self)
end

def next_ordinal
map(&:ordinal).max + 1
end
end
``````

Now that we've created the `Deck` and `CardStack` classes, let's jump back to the `Card` class and add a method for moving cards.

First it verifies that it and the destination stack are members of the same deck.

Then it updates its stack.

As well as updating its ordinal so that it is the last card in the new stack.

``````class Card
# ...
def move_to(dest_stack)
deck == dest_stack.deck or
fail "Can't move card to a different deck"
self.stack   = dest_stack
self.ordinal = dest_stack.next_ordinal
end
end
``````

Now that we know how to properly move a card, let's revisit the `CardStack=` class and add a method for dealing out a given number of cards to another stack.

``````class CardStack
# ...
def deal(count, to:)
count.times do
first.move_to(to)
end
end
end
``````

Now, ultimately we're going to want a number of different domain actions for interacting with stacks of cards.

But just to give the flavor of what it's like to code for this new model of a card game, let's write a method for discarding a card.

Inside, move this card to a stack named `:discard_pile`.

``````class Card
# ...
end
end
``````

Now let's see this method in action.

We'll start by seeding the random number generator, just for the sake of a consistent demonstration.

Then we'll instantiate a new `Deck` for this game.

We'll deal hands for Mal, Wash, Kaylee, and Zoe.

Let's take a look at the first card in Mal's hand.

Now let's discard it.

Listing Mal's hand, we can see that the card is gone.

Listing the discard pile, we can see the card has taken up residence.

``````require "./card"
require "./deck"
require "./card_stack"

srand 123

deck = Deck.new

mal  = deck.stack(:mal_hand)
deck.stack(:draw_pile).deal 5, to: mal

wash = deck.stack(:wash_hand)
deck.stack(:draw_pile).deal 5, to: wash

kaylee = deck.stack(:kaylee_hand)
deck.stack(:draw_pile).deal 5, to: kaylee

zoe = deck.stack(:zoe_hand)
deck.stack(:draw_pile).deal 5, to: zoe

mal.to_a
# => [Card(Jack of Hearts), Card(6 of Spades), Card(King of Diamonds), Card(7 of Clubs), Card(8 of Clubs)]

mal.to_a
# => [Card(6 of Spades), Card(King of Diamonds), Card(7 of Clubs), Card(8 of Clubs)]

# => [Card(Jack of Hearts)]
``````

This is kind of anticlimactic, but it represents a huge change in how we think about the problem. We started with a set of disconnected collections, which could easily become inconsistent with each other. Now, we have a closed system. Our collections are virtual, a card can only belong to one of them at a time, and all of the card movements which take place happen in the context of a single, internally-consistent `Deck`.

And our new domain model doesn't just make it easier to avoid inconsistencies. Let's say we decide we want to be able to save the state of a game in a file or database.

We've made this job very easy. If we simply dump the entries of the `Deck` to storage, we have a nearly complete snapshot of the game in progress.

``````require "yaml"
puts deck.to_yaml

# >> --- &1 !ruby/object:Deck
# >> cards:
# >> - !ruby/struct:Card
# >>   suit: Hearts
# >>   rank: 1
# >>   stack: !ruby/object:CardStack
# >>     deck: *1
# >>     name: :draw_pile
# >>     hash: -300291496390863507
# >>   ordinal: 36
# >> - !ruby/struct:Card
# >>   suit: Hearts
# >>   rank: 2
# >>   stack: !ruby/object:CardStack
# >>     deck: *1
# >>     name: :draw_pile
# >>     hash: -300291496390863507
# >>   ordinal: 26
# ...
``````

The only extra information we need to store is whose turn it is.

Now, I know that unless you work for the online gaming industry, you're not likely to need to model this particular domain for your day job.

But this is a type of modeling problem I run into surprisingly often, in many different domains. There's a set of simple objects which seem like classic Value Objects. Except that, they need to exist inside a closed system, where a given object can only belong to one collection at a time.

The next time you run into a situation that involves this kind of fixed pool of objects, you'll remember this playing cards example. I can't tell you that it's definitely the right solution. But if nothing else, it's an alternative worth considering. Happy hacking!