オブジェクト指向プログラムに挑戦

せっかく Ruby で遊ぶのですから、オブジェクト指向プログラムも作ってみましょう。オブジェクトとは何かという問の答えは専門家でも論争があるようなので、ここでは簡単に問い合わせに答えてくれる「物」だと考えましょう。例えば一台の自動車はオブジェクトです。アクセルを踏むと、スピードを挙げるし、ハンドルを切ると向きをかえます。また、現在位置を問い合わせると、それを教えてくれます。

また、クラスとは物を作るための設計図のようなものだと考えることにします。例えば、個々の自動車はそれぞれに異なっていますが、どの自動車もハンドルや車輪やドアなど共通するものがあります。そのどの自動車にも共通しているものを集めて来て、これは自動車のクラスであるとするのです。逆にどの自動車もクラスで定義された属性である、ハンドルや車輪やドアを持っています。ただ、ハンドルの形や車輪の大きさドアの数等は個々の自動車でまちまちなのです。

プログラムで作るオブジェクトも上の自動車の場合と同じです。プログラムで実現するオブジェクトには二つの要素があります。一つはそのオブジェクトの内部状態であるインスタンス変数です。これは自動車の、ハンドルや車輪に相当します。もう一つはオブジェクトが外部からの問い合わせに答えるためのメソッドです。自動車の場合はハンドルを切ることや、アクセルを踏むことに相当します。しかし、抽象的に説明しようとするとますます分からなくなりそうなのでとにかくプログラムを作ってみましょう。

雑誌などの占いや適性診断の記事で、次の例のように。問に対して答えを選択すると、その選択によって次の問が指定され、その問を選択するとまた次の問になる、と言う形で結論まで辿っていくものがあります。

[質問1] 朝は早く起きるほうである。[答え] はい(質問2へ) いいえ(質問4へ)
[質問2] 体を動かすのが好きである。[答え] はい(質問6へ) いいえ(質問9へ)
.......
[判定A] あなたの生活は健康的です。

これをプログラムにするとどうなるでしょうか。最初にイメージとして一つの問といくつかの答えの選択肢が書いてある一枚のカードを想像します。また、このカードには選択肢のどれを選ぶかによって、次はどのカードをめくるかの指示が書いてあるとします。このカードで遊ぶには、まず最初のカードをめくって問を読み、その答えの選択肢のなかから一つ選びます。そこで、その選択肢によって次にどのカードをめくるかが示してありますからその指示にしたがって次のカードをめくります。その操作を続けていくと、最終的な判定を見ることができます。このカードの中身は様々ですが、共通しているのは問と選択肢と次のカードへのリンクがあると言うことです。そこで、次のように質問カードのクラス Card を定義します。

class Card
  def initialize( question, choice )
    @question = question
    @choice = choice
    @link = []
  end
  def link( next_card )
    @link = next_card
  end
  def display
    puts @question
    if ( @choice != [] )
      for i in 0...@choice.length
        print "#{i}) #{@choice[i]}\t"
      end
      print "\nPlease chose the number: "
      ans = gets.to_i
      @link[ ans ]
    else
      nil
    end
  end
end

上のプログラムを上から順に説明すると、まず class Card で質問カードのクラスの名前を Card と定義します。クラスの名前の先頭の文字は必ず大文字にします。クラスを一言で説明すると、オブジェクトの設計図です。例えば、このプログラムにしても質問カードを1枚だけしか作らないと言うことはありません。何枚かのカードをつくってそれぞれ関連づけて使うでしょう。したがって、あらかじめカードに共通な性質をまとめた設計図を作っておいて、そこから新しいカードを作り出すという形式にすると便利です。

クラスとオブジェクトを別の観点からも考えてみましょう。オブジェクト指向機能のないプログラム言語でも、文字列とか整数などのような単体のデータだけでなく、配列や構造体など、単体のデータを組み合わせたものを一つのデータ単位として扱うことがあります。オブジェクトもこの配列や構造体と基本的には同じものです。ただし、オブジェクトはメソッドもそのなかに含んでいます。そうしてオブジェクトの中のデータの操作はそのオブジェクトのメソッドに問い合わせをする事によってのみ行われます。これは、操作の制限ではなくてプログラムの変更を容易にするための方法です。例えば、図書館で本を借りるとき受付で本を依頼すれば、その図書館の本棚の配列がどうかを知っている必要がありません。また、図書館も本棚の配列を変更した場合でもユーザーが本を捜せなくなると言うことがなくなるわけです。

このプログラムの場合も一枚のカード・オブジェクトを作るためにまずクラスを定義します。クラスの定義ではカードの名称、質問、選択肢とリンクのリストなどを納めておくインスタンス変数を定義します。そうして、それらのインスタンス変数を変更したり処理してデータを外部へ引き渡すメソッドも定義します。クラスの定義は class 'クラス名' で始まり end キーワードで終了します。そうして新しいカードを作るときは、a = クラス名.new( 引数, ... )というように、new メソッドを呼び出して作ります。その後はオブジェクト a にたいして a.methodName というように a のメソッドを利用することで a を操作することができます。

クラス定義の最初には一般に、オブジェクトの初期化を行うメソッド initialize の定義をします。

  def initialize( question, choice )
    @question = question
    @choice = choice
    @link = []
  end

この initialize メソッドはあとで、a = Card.new( "Are you OK?", [ "Yes", "No" ] )のように質問カードのオブジェクトを new メソッドで作るときに実行されるメソッドです。この場合は new で新しいカードを作るときに質問と選択肢のリストを引数に取るようにします。@ マークのついた変数はインスタンス変数です。インスタンス変数は一個のオブジェクトが保持している情報です。カードには、質問と選択肢と次のカードへのリンクが書き込まれるはずですから、Card クラスのインスタンス変数はそれぞれに対応して @question, @choice, @link の三つがあります。上のオブジェクト a の場合は、@question = "Are you OK?", @choce = [ "Yes", "No" ], @link = [] となります。@link が空なのはリンクすべき他のカードがまだ作成されていないからです。

インスタンス変数の先頭に @ がついているのは、インスタンス変数がオブジェクトの外部からは値を参照できないからです。インスタンス変数の値を変更したり、参照したりするためにはクラス定義でそういう操作を行うメソッドを定義しなければなりません。オブジェクトに対するアクセスをメソッドだけに制限してカプセル化するメリットについては上に述べました。オブジェクトに対する操作は必ずメソッドを介するものだと言うことが習慣になると、オブジェクト指向プログラムはそう難解なものではなくなります。

次のメソッド link はカード・オブジェクトに link リストを書き込むときに使用されます。

  def link( next_card )
    @link = next_card
  end

たとえば、カード a の選択肢 0 番にはカード b をリンクし、選択肢 1 番にはカード c をリンクしたいときは、 a.link( [ b, c ] ) とします。

次のメソッド display はカードの中身を表示して、選択肢の入力を促す仕事をします。puts @question で質問を表示し、if (@choice != [])で選択肢があるかどうか判断します。選択肢があれば、for i in .. 〜 end のループで選択肢を番号つきで表示し、print "\nPlease .. " 以下で選択肢の入力を促します。最後に @link [ ans ]で次のカードのオブジェクトを戻り値として返します。else 以下は @choice == [] の時の処理で戻り値 nil を返します。

  def display
    puts @question
    if ( @choice != [] )
      for i in 0...@choice.length
        print "#{i}) #{@choice[i]}\t"
      end
      print "\nPlease chose the number: "
      ans = gets.to_i
      @link[ ans ]
    else
      nil
    end
  end

メインルーチンは非常に簡単です。new メソッドで新しいカードを作成し、link メソッドで選択肢に対して次のカードを関連づけます。

a = Card.new("Which do you like better, Yuki or Mai?", [ "Yuki", "Mai" ])
b = Card.new("Hi, I am Yuki.", [])
c = Card.new("Hi, I am Mai.", [])
a.link( [ b, c ] )

メインループも簡単です。current に 最初のカード a を代入します。current オブジェクトに display メッセージを問い合わせると、選択肢の種類によって次のカードを返しますから それを nxt に代入し、つぎに current に nxt を代入してループを繰り返します。current の内容がカードオブジェクトではなくて nil の時、ループは終了します。

current = a
while (current)
  nxt = current.display
  current = nxt
end

出来上がったプログラムの全体は次のようになります。これを card.rb という名前のファイルにします。

class Card
  def initialize( question, choice )
    @question = question
    @choice = choice
    @link = []
  end
  def link( next_card )
    @link = next_card
  end
  def display
    puts @question
    if ( @choice != [] )
      for i in 0...@choice.length
        print "#{i}) #{@choice[i]}\t"
      end
      print "\nPlease chose the number: "
      ans = gets.to_i
      @link[ ans ]
    else
      nil
    end
  end
end

a = Card.new("Which do you like better, Yuki or Mai?", [ "Yuki", "Mai" ])
b = Card.new("Hi, I am Yuki.", [])
c = Card.new("Hi, I am Mai.", [])
a.link( [ b, c ] )

current = a
while (current)
  nxt = current.display
  current = nxt
end

実行結果は次のようになります。

$ ruby card.rb
Which do you like better, Yuki or Mai?
0) Yuki 1) Mai
Please chose the number: 1
Hi, I am Mai.

クラス定義の部分は再利用できそうなのでこの部分だけを抜きだして Card.rb というファイルにしておきます。また、メイン部分のループも再利用したいので、これを start というメソッドにして Card.rb に一緒に記述します。

class Card
  def initialize( question, choice )
    @question = question
    @choice = choice
    @link = []
  end
  def link( next_card )
    @link = next_card
  end
  def display
    puts @question
    if ( @choice != [] )
      for i in 0...@choice.length
        print "#{i}) #{@choice[i]}\t"
      end
      print "\nPlease chose the number: "
      ans = gets.to_i
      @link[ ans ]
    else
      nil
    end
  end
end

def start( card )
  while ( card )
    card = card.display
  end
end

Card.rb を再利用した場合のスクリプト card2.rb は次のようになります。先頭の行の require 文が Card.rb を利用するための処理です。

require "Card.rb"

a = Card.new("Which do you like better, Yuki or Mai?", [ "Yuki", "Mai" ])
b = Card.new("Hi, I am Yuki.", ["Hi"])
c = Card.new("Hi, I am Mai.", ["Hi"])
d = Card.new("Do you like my music?", [ "Yes", "No" ])
e = Card.new("Thank you!", [])
f = Card.new("It's a shame.", [])

a.link( [ b, c ] )
b.link( [ d ] )
c.link( [ d ] )
d.link( [ e, f ] )

start( a )

実行してみましょう。

$ ruby card2.rb 
Which do you like better, Yuki or Mai?
0) Yuki 1) Mai
Please chose the number: 0
Hi, I am Yuki.
0) Hi
Please chose the number: 0
Do you like my music?
0) Yes  1) No
Please chose the number: 0
Thank you!

Ruby でオブジェクト指向プログラムを書くと、まるで、実際のカードを作っているような感覚でコードを書いていくことができます。こんなにあっさりとプログラムが作れて良いのだろうかという気持にさえなります。いままでずっと、コンピュータを動かすためではなく、自分の頭のなかにあるアイディアを実体化するために使えるプログラム言語はないものだろうかと捜していましたが、ひょっとすると、Ruby が求めていたプログラム言語かも知れません。