Tip
Don't use parallel structures
Two parallel structures should be replaced with tuples or some kind of proper collection.
Mapping to a tuple of values
The following is the essence of how mapping is done to a two-tuple:
class_, rank_str= { 1: (AceCard,'A'), 11: (FaceCard,'J'), 12: (FaceCard,'Q'), 13: (FaceCard,'K'), }.get(rank, (NumberCard, str(rank))) return class_( rank_str, suit )
This is reasonably pleasant. It's not much code to sort out the special cases of playing cards. We will see how it could be modified or expanded if we need to alter the Card
class hierarchy to add additional subclasses of Card
.
It does feel odd to map a rank
value to a class
object and just one of the two arguments to that class initializer. It seems more sensible to map the rank to a simple class or function object without the clutter of providing some (but not all) of the arguments.
The partial function solution
Rather than map to a two-tuple of function and one of the arguments, we can create a partial()
function. This is a function that already has some (but not all) of its arguments provided. We'll use the partial()
function from the functools
library to create a partial of a class with the rank
argument.
The following is a mapping from rank
to a partial()
function that can be used for object construction:
from functools import partial part_class= { 1: partial(AceCard,'A'), 11: partial(FaceCard,'J'), 12: partial(FaceCard,'Q'), 13: partial(FaceCard,'K'), }.get(rank, partial(NumberCard, str(rank))) return part_class( suit )
The mapping associates a rank
object with a partial()
function that is assigned to part_class
. This partial()
function can then be applied to the suit
object to create the final object. The use of partial()
functions is a common technique for functional programming. It works in this specific situation where we have a function instead of an object method.
In general, however, partial()
functions aren't helpful for most object-oriented programming. Rather than create partial()
functions, we can simply update the methods of a class to accept the arguments in different combinations. A partial()
function is similar to creating a fluent interface for object construction.
Fluent APIs for factories
In some cases, we design a class where there's a defined order for method usage. Evaluating methods sequentially is very much like creating a partial()
function.
We might have x.a().b()
in an object notation. We can think of it as . The x.a()
function is a kind of partial()
function that's waiting for b()
. We can think of this as if it were .
The idea here is that Python offers us two alternatives for managing a state. We can either update an object or create a partial()
function that is (in a way) stateful. Because of this equivalence, we can rewrite a partial()
function into a fluent factory object. We make the setting of the rank
object a fluent method that returns self
. Setting the suit
object will actually create the Card
instance.
The following is a fluent Card
factory class with two method functions that must be used in a specific order:
class CardFactory: def rank( self, rank ): self.class_, self.rank_str= { 1:(AceCard,'A'), 11:(FaceCard,'J'), 12:(FaceCard,'Q'), 13:(FaceCard,'K'), }.get(rank, (NumberCard, str(rank))) return self def suit( self, suit ): return self.class_( self.rank_str, suit )
The rank()
method updates the state of the constructor, and the suit()
method actually creates the final Card
object.
This factory class can be used as follows:
card8 = CardFactory() deck8 = [card8.rank(r+1).suit(s) for r in range(13) for s in (Club, Diamond, Heart, Spade)]
First, we create a factory instance, then we use that instance to create Card
instances. This doesn't materially change how __init__()
itself works in the Card
class hierarchy. It does, however, change the way that our client application creates objects.