天天看點

《Python面向對象程式設計指南》——1.7 簡單的組合對象

本節書摘來自異步社群《python面向對象程式設計指南》一書中的第1章,第1.7節,作者[美]steven f. lott, 張心韬 蘭亮 譯,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

一個組合對象也可以稱作容器。我們會從一個簡單的組合對象開始介紹:一副牌。這是一個基本的集合對象。我們的确可以簡單地使用一個list來代替一副牌(deck)對象。

在設計一個類之前,我們需要考慮這樣的一個問題:簡單地使用list是合适的做法嗎?

可以使用random.shuffle()函數完成洗牌操作,使用deck.pop()來完成發牌操作。

一些程式員可能會過早定義新類,正如像使用内置類一樣,違反了一些面向對象的設計原則。比如像下面的這個設計。

可如果業務邏輯這麼簡單的話,為什麼要定義新類?

這裡沒有明确的答案。類定義的一個優勢是:類給對象提供了簡單的、不需要實作的接口。正如之前在對工廠的設計讨論時所看到的,對于python來說,類并不是必需的。

在之前的例子中,有兩個關于deck的使用執行個體而且類定義似乎并不能過于簡化。這有個很大的好處是它隐藏了具體的實作。而由于細節過于細微是以暴露它們并不需要太高的維護成本。本章主要專注于__init__()方法,是以接下來會讨論一些關于如何建立和初始化一個集合的設計。

設計集合類,通常有如下3種政策。

封裝:這個設計是基于現有集合類來定義一個新類,屬于外觀模式的一個使用場景。

擴充:這個設計是對現有集合類進行擴充,通常使用定義子類的方式來實作。

建立:即重新設計。在第6章“建立容器和集合”中我們會深入探讨。

這3個方面是面向對象設計的核心。我們在設計一個類時,總需要謹慎考慮再做出選擇。

以下是對内部集合進行封裝的設計。

我們已經定義了deck類,内部實際調用的是list對象。deck類的pop()方法隻是對list對象相應函數的調用。

我們可以使用以下代碼來建立一個hand對象:

一般來說,外觀模式或者封裝類中的方法實作隻是對底層對象相應函數的代理調用。有時候這樣的代理未免顯得有些多餘,因為對于複雜的集合,我們需要代理大量的函數來更完整地封裝這個底層對象。

類設計的另一個選擇是擴充現有類。這樣做的好處是不需要再重新實作已有的pop()方法了,隻需簡單地繼承即可。重用pop()方法的好處是,無需編寫太多代碼就可以建立一個類。在這個例子中,擴充list類引入了很多我們實際并不需要的函數。

以下代碼示範了基于對内部集合類擴充的deck類的定義。

在一些情形下,在子類中需要顯式調用基類的函數來完成适當的實作。關于這一點,在接下來的章節中會看到其他一些例子。

我們使用了基類中的__init__()函數來初始化list對象進而構造了一個對象集合。然後進行洗牌操作。pop()函數隻需繼承自list集合就可以很好地工作了,其他函數也一樣。

在玩牌時,牌通常會從一個發牌機中取出,這個容器通常包含了混在一起的6副牌。這樣就需要我們來建立一個自定義的deck類而不再隻是簡單地從list對象繼承。

進一步說,發牌機并未完全發牌,而是插入一個标記牌。由于有一張标記牌,有些牌就被有效地分開了。

以下是一個deck類的定義,包含了多副牌,每副牌有52張牌。

這裡我們使用了基類的__init__()函數來建立一個空集合。然後調用self.extend()函數來把多副牌加載到發牌機中。由于我們沒有在子類重寫super().extend()函數,因為我們也可以直接調用基類中相應的實作。

我們也可以使用更底層的表達式生成器通過調用super().__init__()函數來實作,如以下代碼所示。

這個類提供了一副牌card執行個體的集合,可以用來模拟21點中的發牌機的發牌過程。

當銷牌時,有一個特殊的過程。在我們設計玩家的紙牌計數政策時,也要考慮到這個細節。