オブジェクト指向

これからもっと大規模なプログラミングに取り組んでいく場合、 オブジェクト指向プログラミングの考え方が重要となる。本格的な オブジェクト指向の考え方を全て理解するのは難しいので、ここでは その入口だけ触れてみよう。

データの内部形式への高依存問題

前回作成したブラック ジャックプログラムを改良することを考えよう。前回は52枚のトランプカー ドを表現する形式を「配列の中のハッシュ」にした。つまり、

01 …… 5152
"suit""n"
"ハート""A"
"suit""n"
"ハート""2"
……
"suit""n"
"クラブ""K"
"suit""n"
"その他""JOKER"

のようなイメージとなる。実際にはこの形式にしておいて困ることは当面考 えられないが、ここでは仮に

効率が悪いのでデータ形式を変える必要が生じた

ものとして話を進めよう。

配列の中のハッシュをやめて、全て通し番号管理 に変えたい。通し番号管理では、

[0][1][2]………… [50][51][52]
22327………… 19334

というデータ格納形式となる。通し番号管理に変えるには、まず、最初に52 枚のカードを作成するメソッドを変える必要がある。これには、もともとの bj.rbにあった createCardsHashArray メソッドを置き換えて、通し番号を 生成するものにすればよい。たとえば、以下のようになるだろう( 前々回のもの と同じ)。

def createCardsArray()
  card=Array.new		# 空の配列を作る
  0.upto(51) do |x|		# ジョーカー不要なので51まで
    card << x
  end
  card				# 最後に card配列 を呼び主に返す
end

今回やろうとしている改造は、カードの内部構造を変えるものなので、 カードを作成するメソッドを変更するのは当然であることが分かる。しかし、 構造を変えたからには、それを利用する部分にも影響が出る。まず、 シャッフルするメソッドを考えよう。

シャッフルするメソッドは、

def shuffle(a)
  srand
  0.upto(a.length-1) do |i|
    j = rand(a.length)    # 交換相手をランダムに選ぶ
    w = a[i]
    a[i] = a[j]
    a[j] = w
  end
  a
end

となっていた。これは仮引数として a を受け取ってはいるも のの、それ(a)がどんな構造になっているかはまったく関知してい ない。たんに配列だと思って値を交換しているに過ぎない。よってこのメソッド は、カードの内部保存形式を変えても全く影響が出ない。

続いて、ブラックジャックのゲームの進行部分を調べてみよう。ブラックジャッ クでは、カードのスートは全く無視して、数のみを見ている。数を見るのは得点 を計算する部分と、画面に表示する部分である。つまり、 dispCard, point 両メソッドに関して、下線で 示した部分を書き換えなければならない。

def dispCard(card)
  printf("%s の %s\n", card["suit"], card["n"])
end


def point(arr)
  ace = 0		# "A"の枚数
  sum = 0		# 合計点
  for card in arr	# arr配列全ての要素に対して繰り返す
    n = card["n"]	# card["n"]で数を表すvalueが得られる
    case n		# nの値によって場合分け
    when "A"		# "A"ならば…
      ace += 1		# 枚数カウンタを1増やしておく
      sum += 11		# まずは11点として加算
    when "10", "J", "Q", "K"
      sum += 10		# 絵札と10は10点
    else		# それ以外(つまり2…9)はそのものを整数にして足す
      sum += n.to_i
    end # when終わり
  end # for終わり
  while sum > 21 && ace > 0 # 21より大きく、かつ、aceが1以上なら
    sum -= 10		# 10を引く
    ace -= 1		# aceを1つ使ったので減らす
  end
  sum
end

さらにもう一点プログラミングに神経を遣わなければならないところがある。 それはカードを配るところで、コンピュータに配るところ

com << cards[cardNo+=1]

と人間に配るところ

player << cards[cardNo+=1]

が、プログラム中何箇所も登場する。これには以下のような問題点が 内在する。

  1. cardsが配列でなくなった場合(たとえばハッシュのとき) には、該当部分を全て書き換えなければならなくなる。

  2. 変数cardNoをいちいち書くのが面倒(変数名を忘れそう)。

  3. cardNoという変数をどこか別の場所で変更したらゲーム がおかしくなる。

  4. 52枚全部引いたとき(cardNo==52に達したとき)どうするか、という処 理をカードを引く箇所全てに書かなければならない。

結局このプログラムを、「通し番号形式」に変えるために 書き換えなければならないところをまとめると bj-numbering.rbとなる。 変更点の多さに着目せよ。

この問題を整理すると以下のようにまとめられる。

このような場合には、カードを使ってゲームを作るときに、

カードってどんな処理ができるものなの?

ということに着目して考える。ブラックジャックゲームを作った経験からは、

ことが最低限必要である。逆に、この条件を満たせば内部構造が、ハッシュ のハッシュだろうが、通し番号の配列だろうが、どんなものでも構わない。

ここで今まで、ArrayやHashを利用した経験を思い出そう。Array(配列)を 使っているときには、我々はただ単に要素を追加したり取り出したりするだけで よく、要素が今何個入っているかとかは普段は意識しなくてよかったし、 並べ換えるときはsortメソッド任せで実際に並べ換えの具体的手 順はいっさい知らなくてよかった。Arrayクラスのメソッドが勝手にやってくれ た。

そこでカードゲームでも同じように、トランプのカードにかかわるいろいろ な処理を持ったCardクラスがあったらいいなと考える。言い換えると、

のような便利なクラスが使えたらいいのに、と考える。

Rubyによるオブジェクト指向プログラミングでは、そのような性質を満たす ものを新しい クラス として宣言し、必要な処理をメソッドとして定義 し、別のプログラムでそれを利用する。具体的には、

  1. カードそのものを表現する Card クラスを定義する
  2. そのクラスの値(インスタンス)を生成し、ゲーム進行部分で利用する

という流れとなる。

Cardクラスを利用したプログラム

以上のことをふまえてブラックジャックを「Cardクラスを使う」方式に作り 直す。手順は、

  1. Cardクラスを定義する
  2. Cardクラスを使うゲーム進行部を作る

の2段階となる。1番をcard.rbに、2番を bj2.rbに作成する。

card.rb

#
# 「カード」を表現するCardクラスを定義する
#
class Card			# クラス名は必ず大文字で始める
  # クラス定義におけるinitializeという名前のメソッドは、
  # そのクラスの値(インスタンス)がnewされるときに自動的に
  # 呼ばれるメソッド。
  # 通常initializeメソッドは必ず定義する。
  def initialize()
    # @で始まる変数はインスタンス変数といい、クラスの中で
    # しか利用できないつまりクラス定義の外からは一切いじる
    # ことも見ることもできないので後で変えるかもしれない
    # データ構造はインスタンス変数に閉じ込めるのがポイント
    @card = Array.new		# 全てのカードを保存する配列
    @now  = -1			# 今何枚目かをしまう変数
    for suit in ["ハート", "スペード", "ダイヤ", "クラブ"]
      @card << {"suit" => suit, "n" => "A"}
      # ここで代入しているのがハッシュ
      2.upto(10) do |n|
	@card << {"suit" => suit, "n" => n.to_s}
      end
      for n in ["J", "Q", "K"]
	@card << {"suit" => suit, "n" => n}
      end
    end
    shuffle
  end

  def shuffle()			# @cardをいじるので引数は要らない
    srand
    0.upto(@card.length-1) do |i|
      j = rand(@card.length)	# 交換相手をランダムに選ぶ
      w = @card[i]
      @card[i] = @card[j]
      @card[j] = w
    end
    @now = -1			# カードの順番-1にしておく。
  end

  def next()			# つぎの1枚を引く、メソッド
    if @now >= @card.length
      shuffle()
    end
    @card[@now += 1]
  end

  # クラスメソッドの定義
  # クラスメソッドはそのクラスのインスタンス全体に共通のメソッド
  # クラスメソッドは  クラス名.メソッド名 で定義する
  def Card.suit(card)		# スートを得るメソッド
    card["suit"]
  end
  def Card.n(card)		# 数を得るメソッド
    card["n"]
  end

end

続いて、上記card.rbを利用したブラックジャック定義 プログラムbj2.rbを示す。先頭にある requireは、別ファイルに書かれたRuby プログラムを取り入れるためのものである。

bj2.rb

#!/usr/koeki/bin/ruby
# coding: utf-8
# ブラックジャック進行部

# Cardクラス定義ファイルを読み込む
require 'card.rb' 	# これでCardクラスが使えるようになる

# ブラックジャックに必要なメソッドの定義
# カードのスートや数を調べるときは、Cardクラスのメソッド
# Card.suit() と Card.n() を利用している。
def dispCard(card)
  # Cardクラスのクラスメソッドを使ってスートと数を得る
  printf("%s の %s\n", Card.suit(card), Card.n(card))
end

def point(arr)
  ace = 0		# "A"の枚数
  sum = 0		# 合計点
  for card in arr	# arr配列全ての要素に対して繰り返す
    n = Card.n(card)	# クラスメソッド経由で数を得る
    case n		# nの値によって場合分け
    when "A"		# "A"ならば…
      ace += 1		# 枚数カウンタを1増やしておく
      sum += 11		# まずは11点として加算
    when "10", "J", "Q", "K"
      sum += 10		# 絵札と10は10点
    else		# それ以外(つまり2…9)はそのものを整数にして足す
      sum += n.to_i
    end # when終わり
  end # for終わり
  while sum > 21 && ace > 0 # 21より大きく、かつ、aceが1以上なら
    sum -= 10		# 10を引く
    ace -= 1		# aceを1つ使ったので減らす
  end
  sum
end

# Cardクラスのインスタンスを生成(既にシャッフルされている)
cards=Card.new

# コンピュータの持ちカードを格納する配列
com=[]
com << cards.next # cardsに属するnextメソッドで次の1枚を引くようにする
com << cards.next

# プレイヤーの持ちカードを格納する配列
player=[]
player << cards.next
player << cards.next

# 最初の手持ちを表示する
puts "わたしの手:"
# コンピュータの1枚目を表示
puts "  [ ??? ]"
# コンピュータの2枚目を表示
dispCard(com[1])	# 2枚目は com[1] に入っている
# プレイヤー側
puts "あなたの手:"
dispCard(player[0])
dispCard(player[1])

# プレイヤーが満足するまで引かせる
while true
  STDERR.print "もう一枚引きますか?(yかnで) "
  answer = gets
  if /^y/i =~ answer
    player << cards.next
    puts "もう一枚引きます。引いたカードはこれです。"
    dispCard(player[-1])
  else
    break		# while true を抜ける
  end
end

# コンピュータが16以下なら引き続ける
while point(com) <= 16
  puts "わたしはもう一枚引きます"
  com << cards.next
end

# いよいよ勝負
puts "勝負!"
puts "わたしの手:"
for c in com		# コンピュータの手持ちカード全て繰り返す
  dispCard(c)
end
puts "あなたの手:"
player.each do |c|	# プレイヤーの手持ちカード全て繰り返す
  dispCard(c)
end

# 得点を比べる
cp, pp = point(com), point(player)
printf("わたし %d : %d あなた\n", cp, pp)

if cp >= 22		# 22以上なら0点ということにしてしまう
  cp = 0		# (コンピュータ)
end

if pp >= 22		# 22以上なら0点ということにしてしまう
  pp = 0		# (プレイヤー)
end

if pp > cp		# コンピュータより点が高ければ
  puts "あなたの勝ちです。参りました!"
else			# そうでなければ(引き分け含む)
  puts "わたしの勝ちです。べろべろばあ…"
end

ゲーム進行部でカードを配るときに、カード番号を逐一管理しなくてよくな る。また、カードの内部保存構造を変えるのが容易になる。実際に変えてみよう。 カードを通し番号管理にしてみる。この場合、Cardクラスの 定義だけを変えるだけでよい。通し番号形式に変えたプログラム card.rbが以下のものである。 上記のものからの変更点を下線で示す。 ブラックジャックゲーム進行部(bj2.rb)は、 以前と全く同じままでよいことに注目せよ。

card.rb(通し番号バージョン)

#
# 「カード」を表現するCardクラスを定義する
#  通し番号方式バージョン
#
class Card
  def initialize()
    @card = Array.new		# 全てのカードを保存する配列
    @now  = -1			# 今何枚目かをしまう変数
    0.upto(51) do |x|		# ジョーカー不要なので51まで
      @card << x
    end
    shuffle
  end

  def shuffle()
    srand
    0.upto(@card.length-1) do |i|
      j = rand(@card.length)    # 交換相手をランダムに選ぶ
      w = @card[i]
      @card[i] = @card[j]
      @card[j] = w
    end
    @now = -1			# カードの順番
  end

  def Card.suit(card)
    s = card / 13
    ["ハート", "スペード", "ダイヤ", "クラブ"][s]
  end
  def Card.n(card)
    n = card%13 + 1
    case n
    when 1
      "A"
    when 11
      "J"
    when 12
      "Q"
    when 13
      "K"
    else
      n.to_s
    end
  end

  def next()
    if @now >= @card.length
      shuffle()
    end
    @card[@now += 1]
  end
end

本日の目次