オブジェクト指向

お財布プログラムの例題

引き続きお財布プログラムを考える。


財布にまつわるお金のやり取りは以下のようにまとめられる。 最初は「入れるのはお金だけ」で考える。

オブジェクト指向では、 このような性質を持ったものをクラスというくくりで考え、 そのクラスには以下のような値や手続きがあると整理して考える。

これをRubyのコードに直すと以下のようになる。

purse.rb

#!/usr/bin/env ruby
class Purse
  def initialize(start=0)	# コンストラクタの定義
    # initializeという名前でメソッド定義しておくと、初期化されるときに
    # 自動的に呼ばれる(それゆえ通常は定義する)。
    # 生成時に呼ばれるメソッドのことを「コンストラクタ」という。
    # @で始まる変数はインスタンス変数といいクラス内ですべてのメソッドから
    # 参照できるもの。@がない変数はメソッド間を越えられない。
    @fund = start	# 引数を指定しなかったら0になる
  end
  def add(amount)
    if amount < 0
      STDERR.printf("マイナスはだめよ(%d)\n", amount)
    else
      @fund += amount
      printf("%d円入れました。\n", amount)
    end
  end
  def sub(amount)
    if amount > @fund
      STDERR.printf("そんなに入っていません(残高: %d)\n", @fund)
    else
      @fund -= amount
      printf("%d円出しました。\n", amount)
    end
  end
  def howmuch()		# 現在の残高を返す
    @fund
  end
end
# ここまでがクラス定義本体

クラス定義も定義しただけでは何も起きず、利用しなければ意味がないのは メソッド定義と同じである。上の purse.rb を利用するプログラムを示す。

purse-use.rb

#!/usr/bin/env ruby
require_relative "purse.rb"		# 同じディレクトリあるときは require_relative

puts "=== 今日からこの財布を使おう。5000円入れておこう: Purse.new(5000)"
saifu = Purse.new(5000)			# .newで定義したクラスの実体を生成する。
# 生成するときに initialize メソッドが呼ばれる。

puts "=== 1598円の買い物をしよう。"
puts "=== 2000円出すぞ: saifu.sub(2000)"
saifu.sub(2000)
puts "=== おつりを402円もらった。しまおう: saifu.add(402)"
saifu.add(402)
puts "=== いまいくらあるんだろう: saifu.howmuch"
printf("残り%d円です\n", saifu.howmuch)

実行例を示す。

./purse-use.rb
=== 今日からこの財布を使おう。5000円入れておこう: Purse.new(5000)
=== 1598円の買い物をしよう。
=== 2000円出すぞ: saifu.sub(2000)
2000円出しました。
=== おつりを402円もらった。しまおう: saifu.add(402)
402円入れました。
=== いまいくらあるんだろう: saifu.howmuch
残り3402円です

クラスとインスタンス

ここで示した財布は「お金だけ出し入れできる」財布で、 そのようなものはいくらでも作れる。ある性質の集合体を設計したものを クラスといい、製品の仕様書に相当する。

クラスは単なる仕様書だが、そこから実際に動くものとして 生み出されたもののことをインスタンス(実体) といい、実際に作られた製品に相当する。一つの仕様書から 需要に応じてたくさんの製品が作られるように、一つのクラスから いくらでもインスタンスを作ることができる。プログラムで 実際に動くものとして利用するのはインスタンスである。 インスタンスをプログラム中で保持する値のことをオブジェクト という。

上のプログラム purse-base.rb の後半の意味を示す。

複数のインスタンス(オブジェクト)

売り物の財布なら、同じ財布を持っている人は世の中にたくさんいる。 同じものではあるが、中に入っているものはそれぞれ違う。 人間も、全人類同じゲノムから生まれるが、一人として同じ人は存在しない。 一つのクラスから生まれるインスタンスはすべて違う個体で、独立した 情報を持ち、それに従った動きをする。

chiharu_saifu = Purse.new(5000)		# 千春さんが財布を新しくして5000円から開始
hibiki_saifu  = Purse.new(8000)		# 響さんが財布を新しくして8000円から開始
yu_saifu      = Purse.new(1000)		# 優さんが財布を新しくして1000円から開始

いずれも、全く同じ設計の財布だが、別個体として存在し続ける。

chiharu_saifu
@fund変数5000 (残高)
addメソッド入れる処理
subメソッド出す処理
howmuchメソッド残高を返す処理
hibiki_saifu
@fund変数8000 (残高)
addメソッド入れる処理
subメソッド出す処理
howmuchメソッド残高を返す処理
yu_saifu
@fund変数1000 (残高)
addメソッド入れる処理
subメソッド出す処理
howmuchメソッド残高を返す処理

これまで使った配列(Array)やハッシュ(Hash)も同様で、 Ruby本体で定義されたクラスを我々は色々な変数に代入して利用している。

カプセル化

クラス定義(classからendまで)の内側で使用された変数はその外側からは 一切見えない。これを変数の隠蔽 といい、プログラム同士が 同じ変数名やメソッド名(まとめてシンボルという)で競合して異常動作を する可能性をなくせる。シンボルを隠蔽してプログラムの一定の部分を独立性を 高めることをカプセル化といい、複数人、 あるいは将来別のプログラムから利用することを見越したプログラムを 作るときには欠かせない。

継承

既に定義されたクラスの基本機能は踏襲しつつ、 機能や性質を追加したい場合に継承 を使う。

たとえば、Purseクラスは現金を入れることしか想定していない。 この機能を保持したまま、「カードも入れられるようにしたい」とする場合、 同じようにクラス設計すると効率が悪い。

purse-with-card.rb

require_relative "purse.rb"		# Purseクラスの定義を読み込む

class PurseWithCard < Purse		# Purseを親として引き継いでクラス定義
  def initialize(money=0, card=[])	# コンストラクタ(.newで呼ばれる)
    super(monkey)			# 親(Purse)の同じ名前のメソッド(initialize)を呼ぶ
    @card = card			# 独自のインスタンス変数を導入
  end
  def putinCard(card)
    @card << card
    printf("%s をしまいました。\n", card)
  end
  def drawCard(card)
    @card.delete(card)
    printf("%s を取り出しました。\n", card)
  end
  def myCards()
    @card.join(",")
  end
end

カードを複数枚配列で保持できるインスタンス変数 @card、 カードの出し入れとリスト返す処理をする3つのメソッド、 putinCard, drawCard, myCards が追加される。

このように、あるクラスを拡張して別のクラスを作ることで、 徐々に複雑さを増した設計をすることができる。

上記のクラス PurseWithCard を利用したプログラム例を示す。

purse-inherit.rb


実行例:

./purse-inherit.rb
=== 今日からこの財布を使おう。5000円入れておこう: Purse.new(5000)
=== 1598円の買い物をしよう。
=== 2000円出すぞ: saifu.sub(2000)
2000円出しました。
=== おつりを402円もらった。しまおう: saifu.add(402)
402円入れました。
=== いまいくらあるんだろう: saifu.howmuch
残り3402円です
=== 次は4000円のものをカードで買おう!
=== このお店は手持ちのカードで買えますか: Pizza,Mustard
=== じゃあPizzaカードで!
Pizza を取り出しました。
=== じゃあPizzaカードで!
=== やったー、手持ちが足りないのに買えた! 帰ろう。
...
=== ただいま。あ、カードがない! 店に忘れた。
Mustard

本日の目次