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

問題提起

Progateで学習を一通り終えても、そこから自分で何かを作ろうとすると途端にできない人を何人も見てきた。この問題に対しては、 「クラスとインスタンスに対しての理解を深めることで突破口を得られるのでは?」という仮説を立てている。

ご存知の通り、Ruby on RailsMVCの流れやモデルを介したデータベースとのやりとりを理解すればとりあえず動作するWebアプリケーションを作ることができる。Ruby on Railsは巧妙にプログラム内部の動きを隠蔽し、マニュアル通りに書いていけばWebアプリケーションができてしまうため、Ruby on Railsの前にRubyを学んだ人の中には「Rubyって一体どこで使うんだろう」という疑問を持つ人さえいる。

例えば、あるルーティングのコードを見てみよう。

# routes.rb
root to: 'posts#index'
resources :posts

get '/login', to: 'sessions#new'

こちらは、ProgateなどでRubyを一通り学んだ後、Railsを学び始めて比較的最初の頃に出てくるコードである。 Railsでは、routes.rbというファイルにて期待されるリクエストとそれに対する処理を決めることができる。

しかしこれを読んでも、学習者たちはなにがなんだかわからない。そもそもクラスもメソッドも定義されておらず、requireもされていない。いきなり、知らないルールで文字が並んでいるのである。 これではとにかく「こう書いたらこう動く」というルールとして覚えるしかなく、なぜRubyを学んだのかその必要性が理解できないのも頷ける。

form_forという、Railsが提供しているformタグを返すメソッドの意味すらも「こう書いたらこう動く」というマニュアル然とした形でしか覚えていなかったりする。そのため少し書き方を間違えてエラーが出ても、エラー文の意味がわからなかったり見当違いの記述を試したりしてしまう。

ここでの問題は、form_forがRailsによって用意されたメソッドであるということをきちんと理解できていないことではない。

「form_forメソッドが引数としてインスタンスを受け取ること」や、「戻り値であるフォーム要素の属性値(class名やアクション属性の値など)が引数のインスタンスの種類や持っている値によって変わること」がきちんとイメージできないということこそが問題だ。これはのちのち、Railsにおける応用的な手法であるフォームオブジェクトやサービスクラスなどについての理解の妨げにもなってしまう。また何より、他の言語でクラスとインスタンスを活用した応用が効かないため、学習効果が半減する。

ただ、Ruby on Railsを早い段階で学ぶことが悪いという訳ではない。自分が書いたコードによりWebアプリが動くこと、とにかく目に見える成果が生まれることは、モチベーションの面では圧倒的に大切なのだ。

Progateでも、クラスとインスタンスを用いたプログラムを作成する方法は内容として確かに存在する。非常に丁寧に説明しており、多くの人が理解できるであろう点は本当に素晴らしい。ただ、そうしたプログラムの例があまりにも少なく、学習者は経験値を積むことができていない。人は数を繰り返すことで少しずつ学びを得る。同じ理屈の問題でも、類問を何度も繰り返し解くことで学びを蓄積できる。得られる学びの量は人によって違うが、まだ理解できていないなと感じたら繰り返しやれば良いだけだ。

本記事の概要

そこで本記事では、できるだけイメージしやすい具体的な業務システムを例にとり、複数のクラスから生み出されたインスタンスが連携してシステムとして動く様を0から作り確認する。

取り組むことで期待できる効果

あるクラスのインスタンス変数が他のクラスのインスタンスを値として持つ、という感覚を養うことができ、同様の形で動くプログラムを自ら考案できるようになる

Ruby on Railsでは、HTTP通信に必要な情報を始めとするWebアプリケーションの動作に必要な全ての情報はインスタンスが保持している。インスタンス変数が他のクラスのインスタンスを格納し、またそのインスタンスが持つインスタンス変数には他のクラスが、、、といった形で情報が整理されている。この記事を読むことで、それがどのように実現されているのかイメージしてもらえらば幸いである。

Railsで出たエラー文を読み解けるようになる

インスタンスメソッドの呼び出しや、インスタンスからの値の取り出し方について慣れることで、エラー文の意味が理解できるようになる効果も期待される。たまに英語がわからないからエラーが読めない、エラーを理解するには英語ができないといけないという意見を目にすることがあるが、プログラムの動きが理解できなければ英語が読めてもエラーは解けない。

コード例がRubyで書かれた名著が読めるようになる

良いプログラムが書けるようになるには、良書をたくさん読むことが大切だ。現在では、より良いプログラムを書くために必要なエッセンスが詰まった良書にRubyを例にとったものがたくさんある。特に以下に挙げる2冊は必読と言ってよい。

オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方

f:id:xeichan:20200113011247p:plain

リファクタリング:Rubyエディション

f:id:xeichan:20200113011306p:plain

これらの本の序章に挙げられている例のコードは、いずれもこれから学ぶプログラムのような、複数のクラスのインスタンスが組み合わさり動作するものとなっている。本記事を読めば、これらの本を読む際はじめの理解がずっとスムーズになり、より深い知識と理論の理解をおし進めることができるだろう。

想定読者

ProgateのRubyの章を全て終えている程度の知識があればこの記事を読み学習することができる。 もちろんRailsを学習している途中でも、Railsチュートリアルを読み終えた程度の段階でもお勧めできる。

作るものの説明

これから作るのは、遊園地の発券機システムである。以下を基本的な要件とする。

  • お客さんが発券機を起動し、会員登録を行う
  • 発券機は、現在遊園地の中にあるアトラクションの一覧を表示する
  • お客さんは、どれか一つを選択する
  • 選択したアトラクションに乗ることができるチケットが発券される

加えて、お客さんが二十歳未満であれば半額に、さらに、その月がお客さんの誕生月だった場合は10%オフになるという仕様も加えてみる。

前提知識: モデリング、モデル化について

クラスとインスタンスの学習で、ある事象や物についてをデータと振る舞いで表現することを学んだと思う。例えば、ProgateのRubyの学習の最後に出てくる料理注文システム。食べ物や飲み物を、名前、カロリー、値段が存在する「もの」としてクラスとインスタンスで表現した。これを、モデリングやモデル化などと呼ぶ。これは、Rubyを用いてプログラムを組む際には基本的に避けて通れないやり方だ。重要なのは、プログラムとして表現したいものにはどのような事物が登場し、それらはどんなデータを持つのか。そして、どのようなことを行うのか。これをしっかり考えることである。具体的な方法については、これからの実践で見ていこう。

実践

先ほど出てきた仕様を踏まえて、早速プログラムを書いてみよう。今回は以下のような手順で書いていくことにする。

  1. プログラムの仕様から、プログラムに登場するものを列挙する
  2. 登場するものの持つ値や状態を考え、クラスとして定義する
  3. プログラムの仕様通りに動くよう記述していく

1. プログラムの仕様からプログラムに登場するものを列挙する

モデリングに「こうしなければならない」という決まったやり方はない。慣れないうちは、最初にプログラムの登場するものを考えてみよう。先ほど説明した作りたいものの動きを「ユースケース」と呼ぶ。

f:id:xeichan:20200118122752p:plain

ユースケースからプログラムを考える時、登場するものは「名詞」として現れる。

今回のユースケースには、以下のようなものが登場する。

  • ユーザー
  • 券売機のシステム
  • アトラクション
  • チケット

f:id:xeichan:20200118122851p:plain

2. 登場するものの持つ値や状態を考え、クラスとして定義する

これらを一つ一つ、クラスとして定義する。任意のディレクトリにmain.rbというファイルを作成し、そこにクラスを定義しよう。

main.rbを作成しクラスを定義しよう

まずはファイルを作成する。以下の通りコマンドを打とう。 これはターミナルからファイルを作成するコマンドだが、もちろんテキストエディタからファイルを作っても良い。

#任意のディレクトリでmain.rbを作成。$以降を打つこと
$ touch main.rb

続いて作成したmain.rbをテキストエディタで開き、以下のように編集する。

# main.rb
# お客さんをモデリングしたクラス
class User
end

# 券売機をモデリングしたクラス
class TicketVendingSystem
end

# アトラクションをモデリングしたクラス
class Ride
end

# チケットをモデリングしたクラス
class Ticket
end

次にそれぞれのクラスの属性について考え、コードに反映させていく。

f:id:xeichan:20200118135059p:plain

f:id:xeichan:20200118135132p:plain

Userクラスについて

Userクラスの属性は以下で定義する。

属性 コード上での名称 想定されるデータの型 説明
名前 name 文字列 ユーザーの名前
パスワード password 文字列 ユーザー認証時に使うパスワード
年齢 age 数字 年齢
誕生月 birth_month 数字 誕生日がある月
チケット tickets 配列 持っているチケット。複数枚ある可能性あり

会員登録とログイン機能でユーザー認証を行うために名前とパスワードが必要。また、今回の仕様では年齢と誕生月によって割引が発生するためそれらの情報も保持できるようにする。チケットについては複数枚持てる仕様にしたいため、空の配列を入れるようにする。

Userクラスの属性をコードに反映させよう

mainl.rbを以下のように編集する。この時、initializeメソッドの引数はキーワード引数で受け取る想定で書いている。

TicketVendingSystemクラスについて

TicketVendingSystemクラス(券売機)の属性は以下で定義する。

属性 コード上での名称 想定されるデータの型 説明
ユーザー一覧 users 配列 システムに登録されているユーザーの配列
アトラクション一覧 rides 配列 遊園地で乗ることができるアトラクションの配列
ログイン中のユーザー current_user ユーザークラスのインスタンス 現在ログインしているユーザー

券売機のシステムは、ユーザーが会員登録を行なったり、乗りたいアトラクションをユーザーに選択させたりする必要がある。また、現在ログインしているユーザーは誰か、という情報も、発券を行う際に必要なため属性として持たせることにした。

TicketVendingSystemクラスの属性をコードに反映させよう

TicketVendingSystemクラスについて、main.rbを以下のように編集する。

Rideクラスについて

アトラクションをモデリングしたRideクラスの属性は以下で定義する。

属性 コード上での名称 想定されるデータの型 説明
アトラクション名 name 文字列 アトラクションの名前
料金 fee 数字 アトラクションの料金。単位は円
ステータス status true/false 現在乗ることができるか否か

ステータスについては、故障や点検などで乗れないことを見越して持たせた。

Rideクラスの属性をコードに反映させよう

Rideクラスについて、main.rbを以下のように編集する。

Ticketクラスについて

最後に、チケットをモデリングしたTicketクラスについて。

属性 コード上での名称 想定されるデータの型 説明
ユーザー user Userクラスのインスタンス 誰のチケットかを示す
アトラクション ride Rideクラスのインスタンス どのアトラクションのチケットかを示す
作成日 created_at Dateクラスのインスタンス いつ発券されたかを示す
料金 fee 数字 料金を示す

発券された日時は、チケットの期限を判定するために利用する。

Ticketクラスの属性をコードに反映させよう

ここまでで、プログラムに登場するものについて、どんな値を持つのか、という情報を定義し終えた。 続いて、仕様通りに動くようにロジックを書いていく。

3. プログラムの仕様通りに動くよう記述していく

ここからは、それぞれのクラスに適切なインスタンスメソッドを定義していくことになる。 仕様では、まずはじめに「遊園地へようこそ!」というメッセージが流れる。この仕様に従うためには、どこかのクラスにputs "遊園地へようこそ!"を実行するインスタンスメソッドを定義すれば良い。では、この動き、振る舞いはいったいどのクラスに持たせるべきだろう。これらのメッセージは券売機のシステムが表示するわけだから、券売機に持たせれば良さそうだ。早速、仕様通りの動作になるようコードを書いていく。

TicketVendingSystemクラスにメッセージを表示するためのメソッドを定義しよう

TicketVendingSystemクラスにshow_first_messageメソッドを定義する。

このプログラムの中に初めてメソッドが定義されたので、動作を確認してみることにする。

main.rbの末尾に、show_first_messageメソッドを実行するコードを書こう

TicketVendingSystemクラスのインスタンスを作成し、そのインスタンスshow_first_messageメソッドを実行するようにする。

まずはTicketVendingSystem.newという記述によって、TicketVendingSystemクラスのインスタンスを生成。 これを変数ticket_vending_systemに代入し、次の行でshow_first_messageを呼び出すように書いている。

では、main.rbを実行して動作を確かめよう。

main.rbを実行しよう

#main.rbのあるディレクトリに移動し、以下のコマンドを実行
$ ruby main.rb
#以下のように表示されればOK
遊園地へようこそ!初めてのご利用ですか?Y/N

初めての利用か否かを選択してもらい処理を分岐しよう

「遊園地へようこそ!初めてのご利用ですか?Y/N」と表示されたあとは、このプログラムを利用している人にYかNを入力してもらい、それを判定して処理を分けなければいけない。もしYと入力されればユーザーの会員登録の処理へ、そうでなければログインの処理へと進むようにする。 まず、ターミナルでユーザーからの入力を受け取るにはgetsメソッドを利用すると良い。getsメソッドの戻り値はユーザーがその場で入力した値になるので、これがYなのかNなのかを判定するコードを書く。書く場所は、一旦show_first_messageメソッドの中にしておく。

chompメソッドは、文字列の最後の改行文字を取り除くメソッド。この後の判定処理の際に改行文字が邪魔になるので、ここで取り除いておく。 続いて、if文で条件分岐を実装する。ユーザーが入力した文字がYであれば新規登録の処理へ、それ以外であればログインの処理へ進むよう書いてみよう。

少し長めのコードを書いたので、ポイントを分けて解説する。

gets.chomp.upcaseについて

ユーザーが「y」の文字を小文字で入力した時のために、文字列のインスタンスが利用できるupcaseメソッドで小文字を大文字に変換する。もし大文字が入力されていた場合は大文字のままになるので、yを小文字で入力されても大文字で入力されても変数inputには大文字のYが代入される。

if文の条件式 if /^Y$/ !~ input について

ここでは正規表現を利用して「ユーザーが入力した文字がYか否か」を判定している。!~はマッチオペレーターと言い、右辺の値が左辺の正規表現とマッチするか確かめることができる。マッチしていれば戻り値としてtrueが、マッチしなければfalseが返るため、ユーザーの入力がyか否かで条件分岐することができる。なお、ここでは本筋からずれるので正規表現!~について説明はしない。詳細については以下のリンク先を、必要に応じて確認してほしい。

正規表現については以下 userweb.mnet.ne.jp

!~については以下 ref.xaio.jp

ユーザー登録の処理を書こう

続いてこのシステムにユーザー登録を行う。先ほどと同様にユーザーの入力を受け取り、それらの情報を元にUserクラスのインスタンスを生成する。

今回も長めのコードを書いたので、ポイントごとに解説。

ユーザー情報の入力 ~ Userクラスのインスタンス生成

長いコードだが、行なっていることはシンプル。ユーザーに質問をし、必要な情報を入力してもらっている。getsメソッドでユーザーが入力した値は必ず文字列になるので、年齢や誕生月については数値に変換するためにto_iメソッドを利用している。その後Userクラスのインスタンスを生成している部分では、キーワード引数を使って必要な情報をinitializeメソッドに渡している。

インスタンス変数@usersへの代入

@usersは、券売機のシステムに登録されたユーザーを保持する配列。今回生成したUserクラスのインスタンス<<メソッドを使ってこの配列に入れる。

会員登録の処理をメソッドに分割しよう

続いてログインの処理を書いていきたいところだが、ちょっと待ってほしい。 一度コードを俯瞰してみると、今作成した会員登録の処理がshow_first_messageという名前のメソッドの中にあるのは不自然である。やっていることと、メソッドの名前が一致していないからだ。これでは他の人が読んだ時に意図が伝わりづらいので、この処理を別のメソッドに切り出そう。

TicketVendingSystemクラスの中にuser_registrationというメソッドを定義し、先ほどの処理をこの中に移動した。そして、show_first_messageメソッドの中でuser_registrationメソッドを呼び出すようにした。呼び出しの処理の部分のself.は省略してもOK。

ただ、これでもshow_first_messageメソッドの中に2つの処理が混ざってしまっている。メッセージを表示する処理と、会員登録かログインかの条件分岐を行う処理だ。

こういった場合、さらに別のメソッドを用意して処理の流れをわかりやすくしてあげるとよい。いきなりshow_first_messageメソッドを呼び出すのではなく、システムの実行を表すexecという別のメソッドを用意してこれを呼び出すことにしよう。

execメソッドを用意し、処理を移そう

execメソッドについて

ここではシステムを実行するというイメージで、execute(実行する)という言葉の略でexecというメソッドを定義した。ここに、これまでshow_first_messageメソッドの中にあった条件分岐の処理を移動したほか、show_first_messageメソッド自体の実行もexecメソッドの中で行うようにした。これにより、まずシステムが実行され最初のメッセージが表示される、次にユーザーの入力によって条件分岐が行われるという流れに対して自然な書き方にすることができた。

ログインの処理を書こう

次は、ログインの処理を書いていく。この券売機のシステムが持つユーザー一覧であるインスタンス変数@usersは配列で、ユーザーのインスタンスが保管されている。利用者が入力する名前とパスワードに一致するインスタンスがあれば、@current_userにそのインスタンスを代入するようにしよう。先ほどの会員登録の処理と同様、別のメソッドに切り出した上でexecメソッドの中で呼び出すようにする。

これで会員登録したユーザーでログインすることができるようになった。

続く

この後は会員登録やログインの処理と同じ要領でshow_menuメソッドを作成し、チケットの購入処理や料金計算のロジックについて記述していく。 長くなってきたので、続きは別で記事にする。

追記

続きはこちら。

joushiki.hatenablog.com