Video transcript & code
On a placid sea, a small buoy bobs gently. it's a simple and unremarkable object. But there is more here than meets the eye.
For the crew of a fishing vessel, the buoy marks the spot where a lobster pot rests on the ocean floor. Using the chain the bouy is attached to, they'll pull in the lobster pot, and add its contents to the days haul.
A lobster buoy is an apt metaphor for a recurring pattern in software engineering: the concept of a "handle". A handle is a simple object which refers to a more complex object or group of objects deep beneath an API.
You might be familiar with the handle concept from the context of system calls for working with files. Operating systems use internal data structures to keep track of information about files. At a deeper level the file system has its own data structures for file housekeeping.
Application developers don't want to deal with the complexity of these deep structures. And operating system programmers don't want applications to depend on internal implementation details.
So instead of exposing the structures directly, operating systems abstract file I/O operations behind a filehandle API. the file handle is typically a simple numeric identifier. By itself, the number is meaningless. We say that it is "opaque". But when we invoke system calls and pass the file handle to them, the operating system is able to map that identifier to its internal tables of files. It can then perform the requested operation without exposing any nitty-gritty implementation details.
The concept of a handle has applicability beyond systems programming.
For instance: In episodes #472 and #473, we developed some objects for managing card games. As we learned more about the contextual identity of card objects, where every card must occupy exactly one collection of cards at a time, we settled on a design centered on a
The deck provides a closed system in which the card objects exist. We can use it to list cards. We can also use it in conjunction with card objects to move cards between various named collections within the deck.
(By the way, in case something looks unfamiliar, we've made some minor tweaks to these classes since they last showed up.)
require "./cards" srand(1234) deck = Deck.new deck.cards_in_stack(:draw_pile).size # => 52 deck.cards_in_stack(:draw_pile).first(5) # => [♣7, ♠Q, ♣2, ♣9, ♠Q] deck.cards_in_stack(:draw_pile).first.move_to(:avdi_hand) deck.cards_in_stack(:draw_pile).size # => 51 deck.cards_in_stack(:avdi_hand) # => [♣7]
Deck interface is usable, clean, and complete. But there are still some drawbacks to it.
- There is something a little awkward and unfamiliar about this interface. In the first attempt we made at managing cards, we represented individual piles and hands of cards as separate array objects. Now, all the cards reside in the deck, and we don't so much move them around as assign them to different names. As an API, this takes some getting used to. We can't really draw on our intuitions from working with real cards in order to work with this interface.
- The fact that everything centers around the deck object is really an implementation detail. It stems from our need to maintain the integrity and consistency of the card game. And yet, any client code is going to develop direct dependencies on this specific implementation architecture that we've chosen.
- Finally, the
Deckclass shows early signs of turning into a "God object". It's easy to see how card manipulation methods will steadily accumulate on it.
For all these reasons, in Episode #463 we didn't introduce this class alone. We also gave it a helper: the
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 deck.next_ordinal_for(name) end def deal(count, to:) count.times do first.move_to(to) end end end
In that episode we glossed over the creation of the
CardStack object relatively quickly. Today, we're going to shine a spotlight on it.
CardStack class is interesting. Typically, we expect an object with a name like "stack" to contain other objects, perhaps using some kind of mutable internal data structure. But our
CardStack is an immutable value object. Its only attributes are a reference to a deck , and the name of the stack within that deck.
deck = Deck.new stack = CardStack[deck, :draw_pile]
Typically, we get a
CardStack by sending the
stack message to the deck.
deck = Deck.new draw_pile = deck.stack(:draw_pile)
CardStack is able to iterate over a subset of cards contained within the
Deck. It can manipulate them as well, with methods like
deal. But the
CardStack itself remains stateless. Instead of being a heavyweight data structure, the
CardStack class gives us a lightweight handle on a concept which was formerly just floating around implicitly inside the
require "./cards" srand(1234) deck = Deck.new draw_pile = deck.stack(:draw_pile) draw_pile.take(4) # => [♣7, ♠Q, ♣2, ♣9] draw_pile.deal(4, to: :avdi_hand) deck.stack(:avdi_hand).to_a # => [♣7, ♠Q, ♣2, ♣9]
From a library implementation point of view, the
CardStack class gives us a home for card-subset-specific logic that would otherwise have bloated up the
Card classes. For instance, cards use ordinal numbers to determine their position in a given stack. There's a utility method on the
Deck class for getting the next ordinal in a given stack.
class Deck # ... def next_ordinal_for(stack) cards_in_stack(stack).map(&:ordinal).max.to_i + 1 end # ... end
This is clearly a stack-centric method. If we move this method to the
CardStack class, not only does it declutter the
Deck class, but since
CardStacks are enumerable the implementation is shorter as well.
CardStack = Value.new(:deck, :name) do # ... def next_ordinal map(&:ordinal).max.to_i + 1 end # ... end
But beyond giving methods like
next_ordinal a better home, the
CardStack class also enables client code to work with cards in a more natural way—and without being tied to a
Deck God object.
For instance, here's a method that takes a
hand stack and a
flop stack, and calculates all the possible 5-card combinations that could result.
def card_combinations(hand, flop) (hand.to_a + flop.to_a).combination(5).to_a end
This method has no dependency on the core
Deck class. All it knows about is the more constrained
CardStack interface. Specifically, that a card stack is something that can be turned into an array. It's ignorant of our closed-deck architecture, and just treats its two inputs as collections of cards.
Strictly speaking, the
CardStack class is unnecessary. But by giving client code a handle object on the concept of card stacks, we've provided a more natural-feeling interface and reduced coupling to our implementation choices.
On a placid sea, a small buoy bobs gently. It's a simple and unremarkable object. But it gives us a handle on a deeper and more complex structure. It's a powerful pattern, that's applicable everywhere from lobster fishing, to operating systems, to application domain object models. Happy hacking!