1.7 简单的组合对象
一个组合对象也可以称作容器。我们会从一个简单的组合对象开始介绍:一副牌。这是一个基本的集合对象。我们的确可以简单地使用一个list来代替一副牌(deck)对象。
在设计一个类之前,我们需要考虑这样的一个问题:简单地使用list是合适的做法吗?
可以使用random.shuffle()函数完成洗牌操作,使用deck.pop()来完成发牌操作。
一些程序员可能会过早定义新类,正如像使用内置类一样,违反了一些面向对象的设计原则。比如像下面的这个设计。
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() ]
可如果业务逻辑这么简单的话,为什么要定义新类?
这里没有明确的答案。类定义的一个优势是:类给对象提供了简单的、不需要实现的接口。正如之前在对工厂的设计讨论时所看到的,对于Python来说,类并不是必需的。
在之前的例子中,有两个关于deck的使用实例而且类定义似乎并不能过于简化。这有个很大的好处是它隐藏了具体的实现。而由于细节过于细微因此暴露它们并不需要太高的维护成本。本章主要专注于__init__()方法,因此接下来会讨论一些关于如何创建和初始化一个集合的设计。
设计集合类,通常有如下3种策略。
- 封装:这个设计是基于现有集合类来定义一个新类,属于外观模式的一个使用场景。
- 扩展:这个设计是对现有集合类进行扩展,通常使用定义子类的方式来实现。
- 创建:即重新设计。在第6章“创建容器和集合”中我们会深入探讨。
这3个方面是面向对象设计的核心。我们在设计一个类时,总需要谨慎考虑再做出选择。
1.7.1 封装集合类
以下是对内部集合进行封装的设计。
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()
我们已经定义了Deck类,内部实际调用的是list对象。Deck类的pop()方法只是对list对象相应函数的调用。
我们可以使用以下代码来创建一个Hand对象:
d= Deck()
hand= [ d.pop(), d.pop() ]
一般来说,外观模式或者封装类中的方法实现只是对底层对象相应函数的代理调用。有时候这样的代理未免显得有些多余,因为对于复杂的集合,我们需要代理大量的函数来更完整地封装这个底层对象。
1.7.2 扩展集合类
类设计的另一个选择是扩展现有类。这样做的好处是不需要再重新实现已有的pop()方法了,只需简单地继承即可。重用pop()方法的好处是,无需编写太多代码就可以创建一个类。在这个例子中,扩展list类引入了很多我们实际并不需要的函数。
以下代码演示了基于对内部集合类扩展的Deck类的定义。
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 )
在一些情形下,在子类中需要显式调用基类的函数来完成适当的实现。关于这一点,在接下来的章节中会看到其他一些例子。
我们使用了基类中的__init__()函数来初始化list对象进而构造了一个对象集合。然后进行洗牌操作。pop()函数只需继承自list集合就可以很好地工作了,其他函数也一样。
1.7.3 可适应更多需求的另一种设计
在玩牌时,牌通常会从一个发牌机中取出,这个容器通常包含了混在一起的6副牌。这样就需要我们来创建一个自定义的Deck类而不再只是简单地从list对象继承。
进一步说,发牌机并未完全发牌,而是插入一个标记牌。由于有一张标记牌,有些牌就被有效地分开了。
以下是一个Deck类的定义,包含了多副牌,每副牌有52张牌。
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()
这里我们使用了基类的__init__()函数来创建一个空集合。然后调用self.extend()函数来把多副牌加载到发牌机中。由于我们没有在子类重写super().extend()函数,因为我们也可以直接调用基类中相应的实现。
我们也可以使用更底层的表达式生成器通过调用super().__init__()函数来实现,如以下代码所示。
( card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart,
Spade) for d in range(decks) )
这个类提供了一副牌Card实例的集合,可以用来模拟21点中的发牌机的发牌过程。
当销牌时,有一个特殊的过程。在我们设计玩家的纸牌计数策略时,也要考虑到这个细节。