示例:扑克牌
用抽象数据类型构建扑克牌、手牌和牌堆的接口,并演示更换实现不影响上层代码
如果我们能用抽象数据类型表示纸牌游戏,那多酷?让我们创建一个比有理数更复杂的接口,能表示牌、手牌和牌堆。有了这些抽象,我们就能玩一些简单的纸牌游戏了!
创建牌
看任何一张牌,标识它为扑克牌的两个属性是点数(rank)和花色(suit)。当然,你还能观察到其他属性,比如长方形形状或塑料质感,但那些不是用来识别牌的重要特征。因此,这是我们的 make-card 构造函数,接受一个 rank 和一个 suit:
(define (make-card rank suit)
(cons rank (first suit)))
这是它的选择函数:
(define (rank card)
(car card))
(define (suit card)
(cdr card))
于是我们可以这样创建一张牌并提取属性:
-> (define c (make-card 13 'heart))
card
-> (rank c)
13
-> (suit c)
h
我们刚创建了红桃 K。
创建手牌
就像现实中一手牌是一组牌,在我们的抽象中,手牌将是牌的列表。我们定义了构造函数和选择函数如下:
(define make-hand list) ;; 构造函数创建牌的列表
(define first-card car) ;; 返回手牌中的第一张牌
(define rest-hand cdr) ;; 返回手牌中其余的牌
(define empty-hand? null?) ;; 检查手中是否没有牌
注意我们把 make-hand 定义成指向过程 list 的变量。这是因为我们不想指定 make-hand 该接受几个实参——我们可以创建任意长度的手牌。我们只想让 make-hand 接受任意数量的牌并存入列表。下面是调用我们 ADT 的一些示例:
-> (define my-hand (make-hand (make-card 1 'heart)
(make-card 5 'diamond)
(make-card 10 'diamond)
(make-card 13 'club)))
my-hand
-> (first-card my-hand)
(1 . h)
-> (rest-hand my-hand)
((5 . d) (10 . d) (13 . c))
使用我们的实现
表示牌就这些!你有牌,也有一组牌。其他一切都能用这两个对象定义。例如,牌堆就是每种点数、花色组合各一张牌的手牌(再加两张王,不过暂时略过)。
现在用我们的实现写些过程。大多数纸牌游戏中,牌的点数代表它的数值。我们来写一个求手牌总点数(total value)的过程。total 接受一手牌,返回所有牌的点数之和。
(define (total hand)
(if (empty-hand? hand)
0
(+ (rank (first-card hand)) (total (rest-hand hand)))))
例子:
-> (total my-hand)
29
更换实现
如果我们改变表示牌的方式会怎样?我们的 total 代码还能工作吗?
答案是肯定的,total 仍能工作,因为有一层抽象把它与牌或手牌的 implementation 隔开。只要我们保持构造函数和选择函数的名字不变,所有基于它构建的过程都会继续工作。假设我们把表示牌的方式改成这样:
(define (make-card rank suit)
(cond ((equal? suit ’heart) rank)
((equal? suit ’spade) (+ rank 13))
((equal? suit ’diamond) (+ rank 26))
((equal? suit ’club) (+ rank 39))
(else (error "say what?")) ))
(define (rank card)
(remainder card 13))
(define (suit card)
(nth (quotient card 13) ’(heart spade diamond club)))
我们的 total 过程在这个实现下同样能工作。在 Racket 解释器里试试!
用这种编程风格,我们能创建更大的程序。