Hard totals, soft totals, and polymorphism

Two classes are polymorphic when they share common attributes and methods. One common example of this is objects of the int and float classes. Both have __add__() methods to implement the + operator. Another example of this is that most collections offer a __len__() method to implement the len() function. The results are produced in different ways, depending on the implementation details.

Let's define Hand so that it will perform a meaningful mixed-class comparison among several subclasses of Hand. As with same-class comparisons, we have to determine precisely what we're going to compare. We'll look at the following three cases:

  • Equality comparisons between Hand instances should compare all cards in the collection. Two hands are equal if all of the cards are equal.
  • Ordering comparisons between two Hand instances should compare an attribute of each Hand object. In the case of Blackjack, we'll want to compare the hard total or soft total of the hand's points.
  • Equality comparisons against an int value should compare the Hand object's points against the int value. In order to have a total, we have to sort out the subtlety of hard totals and soft totals in the game of Blackjack.

When there's an ace in a hand, then the following are two candidate totals:

  • The soft total treats an ace as 11. 
  • The hard total treats an ace as 1. If the soft total is over 21, then only the hard total is relevant to the game.

This means that the hand's total isn't a simple sum of the cards.

We have to determine whether there's an ace in the hand first. Given that information, we can determine whether there's a valid (less than or equal to 21) soft total. Otherwise, we'll fall back on the hard total.

One symptom of Pretty Poor Polymorphism is the reliance on isinstance() to determine the subclass membership. Generally, this is a violation of the basic ideas of encapsulation and class design. A good set of polymorphic subclass definitions should be completely equivalent with the same method signatures. Ideally, the class definitions are also opaque; we don't need to look inside the class definition. A poor set of polymorphic classes uses extensive isinstance() class testing.

In Python, some uses of the isinstance() function are necessary. This will arise when using a built-in class. It arises because we can't retroactively add method functions to built-in classes, and it might not be worth the effort of subclassing them to add a polymorphism helper method.

In some of the special methods, it's necessary to use the isinstance() function to implement operations that work across multiple classes of objects where there's no simple inheritance hierarchy. We'll show you an idiomatic use of isinstance() for unrelated classes in the next section.

For our cards class hierarchy, we want a method (or an attribute) that identifies an ace without having to use isinstance(). A well-designed method or attribute can help to make a variety of classes properly polymorphic. The idea is to provide a variant attribute value or method implementation that varies based on the class.

We have two general design choices for supporting polymorphism:

  • Define a class-level attribute in all relevant classes with a distinct value.
  • Define a method in all classes with distinct behavior.

In a situation where the hard total and soft total for the cards differ by 10, this is an indication of at least one ace being present in the hand. We don't need to break encapsulation by checking for class membership. The values of the attributes provide all the information required.

When card.soft != card.hard, this is sufficient information to work out the hard total versus the soft total of the hand. Besides indicating the presence of AceCard, it also provides the exact offset value between hard and soft totals. 

The following is a version of the total method that makes use of the soft versus hard delta value:

def total(self) -> int:
delta_soft = max(c.soft - c.hard for c in self.cards)
hard = sum(c.hard for c in self.cards)
if hard + delta_soft <= 21:
return hard + delta_soft
return hard

We'll compute the largest difference between the hard and soft total of each individual card in the hand as delta_soft. For most cards, the difference is zero. For an ace, the difference will be nonzero.

Given the hard total and delta_soft, we can determine which total to return. If hard+delta_soft is less than or equal to 21, the value is the soft total. If the soft total is greater than 21, then revert to a hard total.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset