hiyoko-programingの日記

プログラミングを勉強したてのひよっ子。   エンジニア目指して勉強中。

モデリングとクラスとインスタンス

モデリングについて

本格的なプログラムを作るにあたって必須となる「モデリング」あるいは「抽象化」。ラスとインスタンスの仕組みを利用してレビューアプリを作ることができたが、その際に作ったクラスは「Review」クラス一つだった。しかし、実際のプログラムでは複数のクラスが連携して動作する。やりたいことをプログラムに落とし込むために必要な工程の一つが、モデリングである。

クラスとインスタンスを使えば世の中に存在する様々な物事やシステムをRubyの世界で表現することができる。といっても、完全に表現しきれる訳ではない。

例えば犬を飼って楽しむゲームを作ろうとして「犬」を表現するためにDogというクラスを作ったとしても、現実世界の犬が持つ情報は多すぎて、全てを表現し切ることは不可能に近い。

実際には、現実世界の犬が持つ情報からシステムに必要な要素のみを抽出して再現することになるだろう。例えば、「犬には性別がある」「犬には犬種がある」「犬は鳴く」「犬は食べる」「犬は歳を取る」といった具合だ。

クラスとインスタンスの仕組みを使ってプログラムを作ろうとすると、データや動作について考えることになる。先の例から考えれば、犬の「性別」「犬種」についてはデータである。そして、「犬は鳴く」「犬は食べる」「犬は歳を取る」といったものが動作だ。オブジェクト指向では、この動作のことを振る舞いと呼ぶ。

そして、作りたいものに合わせて現実の犬から必要なデータや振る舞いを選択することを「モデリング」あるいは「抽象化」と呼ぶ。

モデリングの肝は取捨選択。システムの仕様を考えた時、どんなデータや振る舞いが求められるかをよく考える必要がある。

例えば犬を飼って楽しむゲームに「犬は歳を取る」という機能は必要なのか?それは、そのゲームの方向性による。もしも「現実世界のペットは必ず死んでしまうので悲しい」という問題を解決するために作られたゲームなのであれば「歳を取る」という振る舞いは必要ないということにもなり得る。

言語を問わず重要な知識

前章で作成した「Review」クラスもそうだが、プログラムの書き手が任意で「データ」と「振る舞い」を合わせてクラスを作れるというRubyの仕様は抽象データ型という考えに基づいている。
他のオブジェクト指向の言語、例えばPHPJavaJavaScriptにおいても同様の「クラスとインスタンス」の仕組みでプログラムを組んでいく。将来別の言語を扱うことになったとしても、原理原則は全く同じ。Rubyを通してこの概念を理解しておけば、別の言語も素早く習得することができる。

モデリング実践

「映画を見るために発券機でチケットを購入する」というシチュエーションをモデリングしてみる。

チケット発券システムを開発

1.動作確認

プログラムを起動する

プログラムを起動。exec.rbはプログラム全体を動かすための起点となるコードなどが書かれたファイル。

ターミナル
1
$ ruby exec.rb

すると、以下のような選択肢が出てくる。

https://tech-master.s3.amazonaws.com/uploads/curriculums//85408ac5fc00e44db2bb308c3c8e1f83.png

3つの映画の選択肢が出て、どれかを選択できる状態になる。

選択肢を選ぶ

例えばここで0と入力しエンターを押すと、以下のようになる。

https://tech-master.s3.amazonaws.com/uploads/curriculums//6b35340a93d1006f27ff3c4c97c9ab51.png

どの映画のチケットを買ったのか、そして所持金がいくら残ったのかが表示されている。

2.要件定義

以上を踏まえて、今回どんなプログラムを作るのか整理すると以下のようになる。

1.実行すると、選択できる映画の一覧が表示され入力待ちになる

2.どれかを選択すると、どのチケットを買っていくら所持金が残ったのか表示される

3.プログラムに登場するものを考える

ここからはクラスとインスタンスを用いてコードを書いていくことを考える。まずは、プログラムに必要なものを考える。

誰かが発券機を利用することは間違いないので、「お客さん」と「発券機」は必要。また、現在上映している映画があるはずなので「映画」と、そして発券される「チケット」も必要。

ということで、「お客さん」「発券機」「映画」「チケット」を用意すべき。

4.それぞれの要素に対応するクラスを用意する

「お客さん」「発券機」「映画」「チケット」に相当するクラスを、それぞれ「Customer」「TicketVendingMachine」「Movie」「Ticket」として用意する。

 以下の指示に従い、4つのクラスを用意する

まずは、デスクトップにフォルダを作成する。

ターミナル
1
2
3
4
5
6
# デスクトップに移動
$cd ~/desktop
# デスクトップにいることを確認
$pwd
#ruby_ticket_systemというフォルダを作成
$mkdir ruby_ticket_system

テキストエディタを開き「ruby_ticket_system」フォルダをOpenし、Commandキー + oで、該当フォルダを選択する。

https://tech-master.s3.amazonaws.com/uploads/curriculums//3a841da994e18441d1cdd547d379afc5.gif

続いて「ruby_ticket_system」の中に「customer.rb」「ticket_vending_machine.rb」「movie.rb」「ticket.rb」を作成。

作成するファイルの中身はそれぞれ以下の通り。

customer.rb
1
2
class Customer
end
ticket_vending_machine.rb
1
2
class TicketVendingMachine
end
movie.rb
1
2
class Movie
end
ticket.rb
1
2
class Ticket
end

全て作成し終わったら、テキストエディタ上では以下のようになっているはず。

https://tech-master.s3.amazonaws.com/uploads/curriculums//8887f73a4eaa5bed7653ff22234d950b.png

6.各クラスのインスタンスが持つ特徴を考えコードに反映させる

続いて、チケット発券システムにおいてそれぞれのクラスがどのようなデータや振る舞いを持つべきかを考える。

movieが持つべきデータ

まずmovie、映画自体は以下のようなデータを持つことにする。

データ コード上での名称
タイトル title
金額 fee
上映開始日 start_date
上映終了日 end_date

続いて、Movieクラスにこの情報を反映させる。Movieクラスのインスタンスが初期化される時は、必ず上記のデータを持たせることにする。

movie.rb
1
2
3
4
5
6
7
8
class Movie
  def initialize(title, fee, start_date, end_date)
    @title = title
    @fee = fee
    @start_date = start_date
    @end_date = end_date
  end
end

これで、MovieクラスがあるファイルでMovie.newとした際に引数で情報を渡し、その値をインスタンス変数に格納することができるようになる。

ticket_vending_machineが持つべきデータ

続いてチケット発券機を表すticket_vending_machineはどうか?

データ コード上での名称
発券できる映画一覧 movies

発券機が持つべきデータは、現在発券可能な映画の情報。発券可能な映画は一つであるとは限らないため、複数のデータをまとめて持てると良い。そこで、moviesというデータは配列を持つことにする。

ticket_vending_machine.rb
1
2
3
4
5
class TicketVendingMachine
  def initialize
    @movies = []
  end
end

後ほどこの配列にMovieクラスのインスタンスが格納されることになる。

ticketが持つべきデータ

続いてticketについて。

データ コード上での名称
タイトル title
金額 fee
期限 limit

実際に発券機でチケットを買うことを想像すると、発券されてくるチケットのタイトルや料金は、観る映画によって変わるはずなので、選択された映画のタイトルや料金を、チケットにそのまま反映させる。そのため、Ticketクラスがinitializeメソッドで初期化される際は必ずMovieクラスのインスタンスを受け取り、そのデータを利用することにする。limitはチケットが利用できる期限。発券された日のみ利用できる、という仕様にしておく。

Movieクラスのインスタンスインスタンス変数@title@feeを持る。しかし、これらタイトルや金額といった情報をTicketクラスの内部から取得するにはどうすれば良いか。Ticketクラスのinitializeメソッドの中では、Movieクラスのインスタンスインスタンス変数を直接参照することはできない。そこで利用するのがゲッター

ゲッター

ゲッターとは、あるクラスのインスタンスインスタンス変数の値を返すだけのメソッドのこと。これを定義することで、インスタンスから自身の持つインスタンス変数の値を取得することができる。

movie.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Movie
  def initialize(title, fee, start_date, end_date)
    @title = title
    @fee = fee
    @start_date = start_date
    @end_date = end_date
  end

  def title
    return @title
  end

  def fee
    return @fee
  end
end

ここで定義したのがゲッター。こうすると、インスタンスは自身のインスタンス変数titleやインスタンス変数feeの値を返す。つまり、movieクラスのインスタンス.titleなどとすることで、それぞれの値を取り出せるようになった。

また、後ほどticket_vending_machineのゲッターも必要となるので、今のうちに定義しておく。

ticket_vending_machine.rb
1
2
3
4
5
6
7
8
9
class TicketVendingMachine
  def initialize
    @movies = []
  end

  def movies
    @movies
  end
end

続いてticket.rbを編集する。

ticket.rb
1
2
3
4
5
6
class Ticket
  def initialize(movie)
    @title = movie.title
    @fee = movie.fee
  end
end

ここで、movie.titlemovie.feeとなっている部分に注目。

ゲッターを定義したことで、movieクラスのインスタンス変数titlefeeの値がそのままticketクラスのインスタンス変数に格納された。

customerが持つべきデータ

最後にお客さんを表すCustomerクラス。

データ コード上での名称
名前 name
所持金 money
チケット ticket

Customerクラスのインスタンスは、チケットを購入した結果最終的にTicketクラスのインスタンスを自らのインスタンス変数ticketに格納する。

customer.rb
1
2
3
4
5
class Customer
  def initialize(name, money, ticket=nil)
    @name, @money, @ticket = name, money, ticket
  end
end

これまで通り初期化の際にインスタンス変数に値をセットしているだけだが、2つばかり新しい文法が登場している。まずは、変数代入時に1行で複数の変数に値をセットするやり方だ。この場合、@nameにnameの値が、@moneyにmoneyの値が、@ticketにticketの値がそれぞれセットされる。名前ではなく、順番によって格納される値が決まっていることに注意しなければならない。
もう一つは、initializeメソッドの引数にあるticket=nilという書き方。これはデフォルト引数といって、もし該当する本引数が無い場合に代わりに=の右辺の値が代入される書き方。こうすることで、もしかしたら引数が送られてこないという想定のメソッドを用意することができるようになる。

7.プログラムを起動するためのファイル「exec.rb」を用意する

今の所ちょっとイメージしづらいかもしれないが、このプログラム全体を起動する準備をしたり、クラスが書かれたファイル達をまとめたりするためにもう一つexec.rbというファイルを作成する。名前は何でも構わないが、execは「execute」の略で「実行する」という意味。

今回のプログラムの実行は、全てこのexec.rb内で行う。exec.rbを起動することで全てのプログラムが動作するようにする。
そのためには、exec.rbに全てのファイルの記述内容が存在することにする必要がある。

require

requireは、あるファイルから他のファイルの記述内容を参照するためのメソッド。
例えばhoge.rbとfuga.rbいうファイルがあった時、fuga.rbにrequire "./hoge"と書くことでfuga.rbにhoge.rbの記述が書かれていることにしてくれる。

「なら始めから一つのファイルに全てのクラスをまとめて書けば良いじゃ無いか」と思うかもしれないが、それではクラスの中身が大きくなってきた時に不便である。実はプログラムはなるべく細かく分かれていた方が管理しやすい。

 

requireメソッドの引数にはファイルの拡張子は必要ない。

ruby_ticket_system/exec.rb
1
2
3
4
require './customer'
require './movie'
require './ticket'
require './ticket_vending_machine'

これでexec.rbを実行した時に全てのファイルが読み込まれるようになった。

8.exec.rbを実行した時に必要な情報がセットされるようにする

今の所、exec.rbを実行しても何も起こらない。そこで、exec.rb内で、これまで作ってきたクラスからインスタンスを作る。

ruby_ticket_system/exec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
require './customer'
require './movie'
require './ticket'
require './ticket_vending_machine'

#customerを作成。nameとmoneyの値を渡している
customer = Customer.new("takashi", 5000)

#ticket_vending_machineを作成。初期値はinitializeメソッドの中で自動で入る
ticket_vending_machine = TicketVendingMachine.new

#movieを3つ作成。start_date、end_dateにはDateクラスのインスタンスを入れる
titanic = Movie.new(
                    "Titanic",
                    2000,
                    Date.new(2017, 2, 20),
                    Date.new(2017, 4, 20)
          )

tom_and_jerry = Movie.new(
                          "tom_and_jerry",
                          1500,
                          Date.new(2017, 3, 18),
                          Date.new(2017, 5, 10)
                )

oceans_eleven = Movie.new(
                          "Ocean's Eleven",
                          1800,
                          Date.new(2017, 4, 18),
                          Date.new(2017, 6, 10)
                )

#作成したticket_vending_machineクラスのインスタンスのmoviesを更新
ticket_vending_machine.movies = [titanic, tom_and_jerry, oceans_eleven]

それぞれのクラスのインスタンスを作成している。その際、いくつか注意すべきポイントがある。

newメソッドに渡している引数について

まず、作成時にnewメソッドに対して必要な引数を渡していることに注目。これらはそれぞれのクラスのinitializeメソッドの引数として渡していた、そのクラスのインスタンスであれば必ず持っていなければならない値。

Dateクラスについて

Movieクラスのインスタンスに渡しているDate.new()に注目すると、これはRubyにもともと備わっているクラスの一つで、日付を表現するためのクラスである。Dateクラスを使って日付を扱えば、うるう年についてなど面倒な日付関連の計算が楽になる。Dateクラスを利用するには明示的にrequireを使って読み込む必要があるためexec.rbを編集する。

Dateクラスのインスタンスには、第一引数から順に年、月、日の順で数字で値を渡します。詳細は、Rubyの公式ドキュメント参照。

Date.civil (Ruby 2.7.0 リファレンスマニュアル)

ruby_ticket_system[1]{5}/exec.rb
1
2
3
4
5
6
7
require './customer'
require './movie'
require './ticket'
require './ticket_vending_machine'
require 'date'

#以下略

インスタンス変数の値の更新について

編集したexec.rbの下の方に、以下のような記述がある。

exec.rb
1
2
#作成したticket_vending_machineクラスのインスタンスのmoviesを更新
ticket_vending_machine.movies = [titanic, tom_and_jerry, oceans_eleven]

結このようにすることでticket_vending_machineのインスタンス変数@moviesの値が[titanic, tom_and_jerry, oceans_eleven]に更新される。

真ん中に=があるということは、「右の値を左の変数に代入する」という式に見える。しかし左辺はticket_vending_machine.moviesとなっており、メソッドのような形。これはどういうことか。

これは、先に出てきたゲッターと関連する文法。TicketVendingMachineクラスに以下のようにメソッドを用意することでインスタンス変数の値の更新を実現している。

ticket_vending_machine.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class TicketVendingMachine
  def initialize
    @movies = []
  end

  def movies
    @movies
  end

  def movies=(movies)
    @movies = movies
  end
end

moviesという文字が出て来過ぎて混乱するが、要するにmovies=という名前のメソッドが受け取った引数の値を@moviesに代入している、ということ。

セッター

インスタンス変数の値を更新するためのインスタンスメソッドのことを、特別にセッターと呼ぶ。

一見「=が一つの代入式」に見えたのは、メソッド名に=が含まれていたから。Rubyはメソッド名であっても半角スペースを無視するため、このようなことができる。

 

これで全てのインスタンスに必要な情報を渡し、初期化することができた。データは揃ったので、これから振る舞いを定義して仕様を実現していく。

9.仕様に沿った動作をする振る舞い(メソッド)を定義する

まずは最初の仕様である「選択できる映画一覧が表示され入力待ちになる」という部分から実装していく。

選択できる映画一覧が表示され入力待ちになる

https://tech-master.s3.amazonaws.com/uploads/curriculums//85408ac5fc00e44db2bb308c3c8e1f83.png

3つの映画の選択肢が出て、どれかを選択できる状態になる。メッセージを出すためにputsメソッドを、ユーザーが選択肢を選べるようにgetsメソッドを使うと良さそうだ。

実装するメソッドの中身は以下のようになる。

1
2
3
4
5
6
7
    puts "どの映画を見ますか?"
    i = 0
    self.movies.each do |movie|
      puts "#{i} #{movie.title}: #{movie.fee}円"
      i += 1
    end
    gets

途中で見慣れないselfというものが出て来た。

self

インスタンスメソッドの中でselfと書くと、そのインスタンスメソッドを使っているインスタンス自身を参照できる。

つまり、self.moviesの返り値は今回の場合[titanic, tom_and_jerry, oceans_eleven]となる。その後、eachメソッドで一つ一つのmovieが参照され、それぞれのタイトルと金額が並んで表示される形になる。

これが実行されれば、ひとまずの仕様は実現できる。ここで問題になるのは、この実装はどのクラスが持つべきかということとどんな名前にするのかということ

どのクラスが持つべきか?

メニューを表示するのは、明確に発券機の仕事なので、この処理はTicketVendingMachineクラスの振る舞いとして定義する。

どんな名前にすべきか?

この処理は映画の一覧を表示しユーザーに選択を促している。また、メニューの表示をしているとも言える。色々な捉え方ができるが、今回は「display_menu」という名前にすることにする。

ticket_vending_machine.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TicketVendingMachine
  def initialize
    @movies = []
  end

  def movies
    @movies
  end

  def movies=(movies)
    @movies = movies
  end

  def display_menu
    puts "どの映画を見ますか?"
    i = 0
    self.movies.each do |movie|
      puts "#{i} #{movie.title}: #{movie.fee}円"
      i += 1
    end
    gets
  end
end

メソッドを定義できたので、exec.rbにdisplay_menuを呼び出す記述を追加する。

exec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
require './customer'
require './movie'
require './ticket'
require './ticket_vending_machine'
require 'date'

#customerを作成。nameとmoneyの値を渡している
customer = Customer.new("takashi", 5000)

#ticket_vending_machineを作成。初期値はinitializeメソッドの中で自動で入る
ticket_vending_machine = TicketVendingMachine.new

#movieを3つ作成。start_date、end_dateにはDateクラスのインスタンスを入れる
titanic = Movie.new(
                    "Titanic",
                    2000,
                    Date.new(2017, 2, 20),
                    Date.new(2017, 4, 20)
          )

tom_and_jerry = Movie.new(
                          "tom_and_jerry",
                          1500,
                          Date.new(2017, 3, 18),
                          Date.new(2017, 5, 10)
                )

oceans_eleven = Movie.new(
                          "Ocean's Eleven",
                          1800,
                          Date.new(2017, 4, 18),
                          Date.new(2017, 6, 10)
                )

#作成したticket_vending_machineクラスのインスタンスのmoviesを更新
ticket_vending_machine.movies = [titanic, tom_and_jerry, oceans_eleven]

ticket_vending_machine.display_menu

ここまで書けたら、一度exec.rbを実行してみる。

ターミナル
1
2
3
4
5
6
7
#exec.rbを実行
$ ruby exec.rb
#以下のように表示されれば成功
どの映画を見ますか?
0 Titanic: 20001 tom_and_jerry: 15002 Ocean's Eleven: 1800

うまく表示されなかった場合、エラーメッセージを読んで解決を試みる。

続いて「どれかを選択すると、どのチケットを買っていくら所持金が残ったのか表示される」という仕様について実装する。

どれかを選択すると、どのチケットを買っていくら所持金が残ったのか表示される

これも、処理を「どのクラスが持つべきか」「どんな名前のメソッドにするか」を考える。

どのクラスが持つべきか

どれかを選択するのは、明確にcustomerの役割なので、映画を選択する、という処理はCustomerクラスに定義すべき。また、どのチケットを買っていくら所持金が残ったのか表示するのは、ticket_vending_machineの役割なので、こちらの処理はTicketVendingMachineクラスに定義する。

どんな名前のメソッドにするか

「映画を選択する」処理なので、「choose_movie」とするのが良い。「どのチケットを買いいくら所持金が残ったのかを表示する」のは「display_result」とする。

ゲッター、セッターを楽に定義する

今回はCustomerクラスにゲッターを実装する。しかし今回はインスタンス変数の数が多く、全てのゲッターを地道に定義すると行数が多くなってしまう。そんな悩みを解決するために、attr_accessorというメソッドがある。

attr_accessor

ゲッター、セッターを簡単に定義できるメソッド。以下のように利用する。

1
2
3
class Dog
  attr_accessor :name, :type, :age
end

これは、以下の定義と同様に評価される。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Dog

  def name
    @name
  end

  def name=(name)
    @name = name
  end

  def type
    @type
  end

  def type=(type)
    @type = type
  end

  def age
    @age
  end

  def age=(age)
    @age = age
  end

end

これだけの行数が削減できるのは非常に便利なので、積極的に使っていくと良い。

名前と処理を置くクラスが決まったので、早速メソッドを定義する。

customer.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Customer
  attr_accessor :name, :money, :ticket

  def initialize(name, money, ticket=nil)
    @name, @money, @ticket = name, money, ticket
  end

  def choose_movie
    gets.to_i
  end
end
ticket_vending_machine.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class TicketVendingMachine
  def initialize
    @movies = []
  end

  def movies
    @movies
  end

  def movies=(movies)
    @movies = movies
  end

  def display_menu
    puts "どの映画を見ますか?"
    i = 0
    self.movies.each do |movie|
      puts "#{i} #{movie.title}: #{movie.fee}円"
      i += 1
    end
    gets
  end

  def display_result(customer)
    chosen_movie = self.movies[customer.choose_movie]
    customer.money -= chosen_movie.fee
    puts "#{chosen_movie.title}のチケットを買ったよ!"
    puts "#{customer.name}の所持金が#{customer.money}円になりました!"
  end
end

これでcustomerが選択した映画の番号によって配列からmovieクラスのインスタンスが取り出され、chosen_movieという変数に代入されるようになる。この時点でdisplay_menuメソッドの中のgetsメソッドは必要がなくなったので削除する。

ticket_vending_machine.rb
1
2
3
4
5
6
7
8
  def display_menu
    puts "どの映画を見ますか?"
    i = 0
    self.movies.each do |movie|
      puts "#{i} #{movie.title}: #{movie.fee}円"
      i += 1
    end
  end

続いて、exec.rbにもdisplay_resultメソッドが実行されるよう編集を加える。

exec.rb
1
2
3
4
5
# 作成したticket_vending_machineクラスのインスタンスのmoviesを更新
ticket_vending_machine.movies = [titanic, tom_and_jerry, oceans_eleven]

ticket_vending_machine.display_menu
ticket_vending_machine.display_result(customer)

これで、exec.rbを実行してみる。

ターミナル
1
2
3
#exec.rbを実行
$ ruby exec.rb
#仕様通り実行されれば成功

10.リファクタリング

コードの仕様を変えずに中身を改善することをリファクタリングと言う。
とりあえず仕様通りに動くものはできたが、コードの内容を改善できる部分がある。例えば、読みやすさ(可読性)。
現状exec.rbの最後の実行部分のコードはdisplay_resultメソッドを利用しているが、display_menuが呼ばれてすぐ次にdisplay_resultが呼ばれているので、何に対しての結果なのかが一見してわかりづらくなっている。

これを改善するには、ユーザーがチケットを買う手順について再考した方が良さそう。

そもそもチケットを買うという行為は誰が主導するのか。今回の場合は、customerの行為になる。つまり、exec.rbの実行部分は以下のように始まるのが正しいと考えられる。

exec.rb
1
2
3
4
#作成したticket_vending_machineクラスのインスタンスのmoviesを更新
ticket_vending_machine.movies = [titanic, tom_and_jerry, oceans_eleven]

customer.buy(ticket_vending_machine)

引数としてticket_vending_machineを渡しているのは、お客さんがチケットを買う券売機を選んで取引を始めるイメージ。こちらの方が、より自然。

では、customer.buyとしてコードを書き換えてみる。

exec.rb
1
2
3
4
5
#省略
#作成したticket_vending_machineクラスのインスタンスのmoviesを更新
ticket_vending_machine.movies = [titanic, tom_and_jerry, oceans_eleven]

customer.buy(ticket_vending_machine)
customer.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Customer
  attr_accessor :name, :money, :ticket

  def initialize(name, money, ticket=nil)
    @name, @money, @ticket = name, money, ticket
  end

  def choose_movie
    gets.to_i
  end

  def buy(ticket_vending_machine)
    ticket_vending_machine.display_menu
    chosen_movie = ticket_vending_machine.movies[choose_movie]
    self.money -= chosen_movie.fee
    self.ticket = Ticket.new(chosen_movie)
    puts "#{chosen_movie.title}のチケットを買ったよ!"
    puts "#{name}の所持金が#{money}円になりました!"
  end
end

このように、同じ仕様を実現するにあたって書けるコードのパターンは無限大である。
加えて、オブジェクト指向という観点においては明確な優劣が存在する。

完成コード

exec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
require './customer'
require './movie'
require './ticket'
require './ticket_vending_machine'
require 'date'

customer = Customer.new("takashi", 5000)

ticket_vending_machine = TicketVendingMachine.new

titanic = Movie.new(
                    "Titanic",
                    2000,
                    Date.new(2017, 2, 20),
                    Date.new(2017, 4, 20)
          )

tom_and_jerry = Movie.new(
                          "tom_and_jerry",
                          1500,
                          Date.new(2017, 3, 18),
                          Date.new(2017, 5, 10)
                )

oceans_eleven = Movie.new(
                          "Ocean's Eleven",
                          1800,
                          Date.new(2017, 4, 18),
                          Date.new(2017, 6, 10)
                )

ticket_vending_machine.movies = [titanic, tom_and_jerry, oceans_eleven]

customer.buy(ticket_vending_machine)
ticket.rb
1
2
3
4
5
6
class Ticket
  def initialize(movie)
    @title = movie.title
    @fee = movie.fee
  end
end
customer.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Customer
  attr_accessor :name, :money, :ticket

  def initialize(name, money, ticket=nil)
    @name, @money, @ticket = name, money, ticket
  end

  def choose_movie
    gets.to_i
  end

  def buy(ticket_vending_machine)
    ticket_vending_machine.display_menu
    choosed_movie = ticket_vending_machine.movies[choose_movie]
    self.money = choosed_movie.fee
    self.ticket = Ticket.new(choosed_movie)
    puts "#{choosed_movie.title}のチケットを買ったよ!"
    puts "#{name}の所持金が#{money}円になりました!"
  end
end
ticket_vending_machine.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class TicketVendingMachine
  def initialize
    @movies = []
  end

  def movies
    @movies
  end

  def movies=(movies)
    @movies = movies
  end

  def display_menu
    puts "どの映画を見ますか?"
    i = 0
    self.movies.each do |movie|
      puts "#{i} #{movie.title}: #{movie.fee}円"
      i += 1
    end
  end

  def display_result(customer)
    choosed_movie = self.movies[customer.choose_movie]
    customer.money -= choosed_movie.fee
    puts "#{choosed_movie.title}のチケットを買ったよ!"
    puts "#{customer.name}の所持金が#{customer.money}円になりました!"
  end
end
movie.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Movie
  def initialize(title, fee, start_date, end_date)
    @title = title
    @fee = fee
    @start_date = start_date
    @end_date = end_date
  end

  def title
    return @title
  end

  def fee
    return @fee
  end
endmode