Simple composite objects
A composite object can also be called a
container. We'll look at a simple composite object: a deck of individual cards. This is a basic collection. Indeed, it's so basic that we can, without too much struggle, use a simple list
as a deck.
Before designing a new class, we need to ask this question: is using a simple list
appropriate?
We can use random.shuffle()
to shuffle the deck and deck.pop()
to deal cards into a player's Hand
.
Some programmers rush to define new classes as if using a built-in class violates some object-oriented design principle. Avoiding a new class leaves us with something as shown in the following code snippet:
d= [card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart, Spade)] random.shuffle(d) hand= [ d.pop(), d.pop() ]
If it's that simple, why write a new class?
The answer isn't perfectly clear. One advantage is that a class offer a simplified, implementation-free interface to the object. As we noted previously, when discussing factories, a class isn't a requirement in Python.
In the preceding code, the deck only has two simple use cases and a class definition doesn't seem to simplify things very much. It does have the advantage of concealing the implementation's details. But the details are so trivial that exposing them seems to have little cost. We're focused primarily on the __init__()
method in this chapter, so we'll look at some designs to create and initialize a collection.
To design a collection of objects, we have the following three general design strategies:
Wrap: This design pattern is an existing collection definition. This might be an example of the Facade design pattern.
Extend: This design pattern is an existing collection class. This is ordinary subclass definition.
Invent: This is designed from scratch. We'll look at this in Chapter 6, Creating Containers and Collections.
These three concepts are central to object-oriented design. We must always make this choice when designing a class.
Wrapping a collection class
The following is a wrapper design that contains an internal collection:
class Deck: def __init__( self ): self._cards = [card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart, Spade)] random.shuffle( self._cards ) def pop( self ): return self._cards.pop()
We've defined Deck
so that the internal collection is a list
object. The pop()
method of Deck
simply delegates to the wrapped list
object.
We can then create a Hand
instance with the following kind of code:
d= Deck() hand= [ d.pop(), d.pop() ]
Generally, a Facade design pattern or wrapper class contains methods that are simply delegated to the underlying implementation class. This delegation can become wordy. For a sophisticated collection, we may wind up delegating a large number of methods to the wrapped object.
Extending a collection class
An alternative to wrapping is to extend a built-in class. By doing this, we have the advantage of not having to reimplement the pop()
method; we can simply inherit it.
The pop()
method has the advantage that it creates a class without writing too much code. In this example, extending the list
class has the disadvantage that this provides many more functions than we truly need.
The following is a definition of Deck
that extends the built-in list
:
class Deck2( list ): def __init__( self ): super().__init__( card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart, Spade) ) random.shuffle( self )
In some cases, our methods will have to explicitly use the superclass methods in order to have proper class behavior. We'll see other examples of this in the following sections.
We leverage the superclass's __init__()
method to populate our list
object with an initial single deck of cards. Then we shuffle the cards. The pop()
method is simply inherited from list
and works perfectly. Other methods inherited from the list
also work.
More requirements and another design
In a casino, the cards are often dealt from a shoe that has half a dozen decks of cards all mingled together. This consideration makes it necessary for us to build our own version of Deck
and not simply use an unadorned list
object.
Additionally, a casino shoe is not dealt fully. Instead, a marker card is inserted. Because of the marker, some cards are effectively set aside and not used for play.
The following is Deck
definition that contains multiple sets of 52-card decks:
class Deck3(list): def __init__(self, decks=1): super().__init__() for i in range(decks): self.extend( card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart, Spade) ) random.shuffle( self ) burn= random.randint(1,52) for i in range(burn): self.pop()
Here, we used the __init__()
superclass to build an empty collection. Then, we used self.extend()
to append multiple 52-card decks to the shoe. We could also use super().extend()
since we did not provide an overriding implementation in this class.
We could also carry out the entire task via super().__init__()
using a more deeply nested generator expression, as shown in the following code snippet:
( card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart, Spade) for d in range(decks) )
This class provides us with a collection of Card
instances that we can use to emulate casino blackjack as dealt from a shoe.
There's a peculiar ritual in a casino where they reveal the burned card. If we're going to design a card-counting player strategy, we might want to emulate this nuance too.