Rubyで学ぶ「ボトムアップ」Webアプリケーション開発その1 クラスとインスタンスを使いこなそう(2)

本記事の概要

本記事は、以下の記事の続編である。

joushiki.hatenablog.com

前回までで、お題である「遊園地の発券機システム」について必要なモデリングを行い、ユーザーがログインできる状態までプログラムを作成した。 本記事では、チケットを購入する処理まで作成していく。

作るもののおさらい

「遊園地の発券機システム」については、大雑把だが以下のような仕様で開発する。

  • 遊園地の発券機システム
  • お客さんが発券機を起動し、会員登録を行う
  • 発券機は、現在遊園地の中にあるアトラクションの一覧を表示する
  • お客さんは、どれか一つを選択する
  • 選択したアトラクションに乗ることができるチケットが発券される
  • お客さんが二十歳未満であれば半額になる
  • その月がお客さんの誕生月だった場合は10%オフになる
  • 上記2つの値引きのロジックは併用可能

チケットの購入処理の際、お客さんが二十歳未満であれば半額なる/その月がお客さんの誕生月だった場合は10%オフになるという仕様についてその通りになるように書く必要がある。条件をメソッドにまとめ、クラス内に定義することにする。

実践

メニューを表示し、入力された値で条件分岐しよう

ユーザーのログイン処理まで作成することができた。続いて「チケットを購入する/システムを終了する」という選択肢を選べるメニューを表示しよう。execメソッドの中でユーザーに0か1かを入力させ、その値によって条件分岐する。

今回は、case文を使って条件分岐を書いた。ユーザーが入力した値が0ならばチケットの購入処理の分岐へ、1ならばシステム終了処理の分岐へ、それ以外であれば、「不正な入力です」と表示させる。

券売機システムにアトラクションの情報を保持させよう

続いて、ユーザーがチケット購入の条件分岐に進んだ場合の処理を書く。その際、発券機のシステム自体がアトラクションの情報を保持しておく必要がある。 今回はあらかじめ、発券機のインスタンスに配列でアトラクション一覧を持たせることにする。 ファイルの中でRideクラスのインスタンスを作り、TicketVendingSystemクラスのインスタンスの「インスタンス変数」に代入しよう。

「ジェットコースター」と」「メリーゴーランド」をイメージしたRideクラスのインスタンスをそれぞれ生成し変数に代入、そのあとTicketVendingSystemクラスのインスタンス変数ridesの配列に入れた。

今乗ることができるアトラクション一覧を表示しよう

今、TicketVendingSystemクラスのインスタンスはアトラクションの情報を保持している。繰り返し処理を使って、「乗れるアトラクションの一覧」を選択肢として表示しよう。

ここで繰り返し時にeach_with_indexというメソッドが使われているので、どういったものか確認しておこう。

each_with_indexメソッドについて

each_with_indexメソッドはeachメソッドに似ている。配列の要素一つ一つを取り出し同様の処理を加えることができるほか、2つめのブロック引数に繰り返しの「回数」が自動で代入される。この時、最初に代入される値は0であることに注意。

これで、システムの利用者には以下のように選択肢が表示されるようになったはずだ。

(画像)

チケットの購入処理を書こう

購入処理を書いていく。今回はTicketクラスのインスタンスを生成しticketという変数に代入する。誰がどのアトラクションに乗るのかなど、必要な情報がticketに渡るようインスタンスの生成時に引数を渡す。 この時に渡す引数は、UserクラスのインスタンスとRideクラスのインスタンスである。これらのインスタンスが、Ticketの生成に必要な情報を持っているからだ。

その後、生成したTicketクラスのインスタンスは、ユーザーのインスタンス変数であるticketsという配列に格納しておく。

チケットの料金について

チケットの料金は、Ticketクラスのインスタンスが生成される際引数として受け取るRideクラスのインスタンスが引き継ぐ形にしている。

さて、チケットの料金については、本当は以下のように決めたい。

  • お客さんが二十歳未満であれば半額になる
  • その月がお客さんの誕生月だった場合は10%オフになる

これらの仕様を実現するために、計算のロジックを作成しメソッドとして定義する。

仕様に合わせてチケットの料金を計算するメソッドを定義しよう

このメソッドはcalculate_feeという名前にし、Ticketクラスに定義する。

calculate_feeメソッドに、ticket自身の料金の計算処理を記載した。 まずはchild?という条件分岐のためのメソッドをUserクラス定義し、返り値がtrueであればRideクラスのインスタンス変数feeの値の半分を、料金として定義している。 返り値がfalseであれば、Rideクラスのインスタンス変数feeの値をそのまま料金として定義する。 続いてbirth_month?という条件分岐のためのメソッドもUserクラスに用意している。こちらは返り値がtrueであれば、feeの値に0.9を掛けることで10%オフを表現している。もしもfalseであれば、何もしない。

calculate_feeの呼び出し方について

calculate_feeメソッドは、Ticketクラスのinitializeメソッドの中で利用するように書いた。 initialzeメソッドの中でのselfは、今まさに定義されたインスタンス自身となる。その前の処理でインスタンス変数@userと@rideは定義されているため、calculate_feeメソッド内部でもこれらのインスタンス変数を利用することができる。

child?とbirth_month?について

child?メソッドとbirth_monthメソッドは、それぞれ新しくUserクラスに定義した。 child?メソッドは、Userクラスのインスタンスが持つ@ageというインスタンス変数の値が19以下であればtrueを、そうでなければfalseを返す。 birth_month?メソッドは、Userクラスのインスタンスが持つ@birth_monthの値と、現在の月(1月、2月の月)が一致していた場合にtrueを、そうでなければfalseを返す。 このように、何か条件分岐を書くときは「条件分岐の判断材料を持つクラスのインスタンスメソッドとして定義できないか」を考えると良い。

floorメソッドについて

floorメソッドは数値に対して利用できるメソッドで、小数点以下について切り捨て整数の値を返してくれる。

これで、最低限のプログラムを完成させることができた。 続いて、プログラムをさらに改善していこう。

calculate_feeを別の場所に移そう

calculate_feeメソッドはチケットの料金を計算するメソッドだが、これはTicketクラスに定義されるべきではない。 新たにFeeCalculatorクラスを作成し、そちらに移動することにする。

FeeCalculatorクラスを作った理由について

単一責任の法則に反しているから

「チケットの料金を計算する」という処理の責任はTicketクラス自体には無いと判断したため、新しくこの責任を持つクラスを作成した。

今後、チケットの料金については様々な仕様が追加されることが考えられる。例えば、以下のようなケースだ。

  • 5才以下は無料にする
  • 期間限定で特定の種類のアトラクションだけ値段が割引される

また、チケットの利用期限などついても様々な仕様が追加されることが予測される。例えば、以下のようなケース。

  • 特定のアトラクションについて、5才以下はそもそも利用できない
  • 回数券や年間パスなど、利用期限が異なった種類のチケットが販売される

料金や利用期限について、さらにチケットについて他の様々なルールがTicketクラスに書き込まれていくと、途端にTicketクラスのメソッドの数が膨れあがり、保守性の悪いクラスになってしまう。

そこで、料金計算の処理については別のクラスを用意しそちらに任せることにした。それが、FeeCalculatorクラスである。

完成

これで一区切りついたので、ひとまずこのシステムについては完成、ということにしたい。もちろんまだ改善の余地は十分にある。テストがないこと。メソッドのサイズがテストを行いやすい単位になっていないこと。挙げればキリがないが、それはまた別の機会に筆を取って修正しようと思う。

終わりに

本記事の目的は、プログラミングを始めたばかりの人がこれを読み、クラスとインスタンスの仕組みを利用したプログラム作成慣れる一助となることだった。 本記事から、モデリングの手段としてユースケースから「データ」と「振る舞い」をもつ「名詞」を抽出する方法を見て取っていただけたなら幸いである。 ただし、このモデリング方法はあくまで1手段でしかない。最後の方で出てきたFeeCalculatorクラスのように、一見「名詞」としては抽出しづらいクラスを定義することこそが、オブジェクト指向プログラミングの難しさであり醍醐味であると筆者は考えている。