学習記録 12日目
12日目の学習記録をまとめていきます。
学習計画
- オブジェクト指向設計実践ガイド 6章
- Railsチュートリアル 2章
学習内容
オブジェクト指向設計実践ガイド 6章
class Bicycle attr_reader :size, :tape_color def initialize(args) @size = args[:size] @tape_color = args[:tape_color] end def spares { chain: '10-speed', tare_size: '23', tape_color: tape_color } end #... end bike = Bicycle.new(size: 'M', tape_color: 'red') bike.size # => 'M' bike.spares # => { chain: "10-speed", # tare_size: "23", # tape_color: "red" }
- ロードバイクのクラスを元にマウンテンバイクのタイプも追加してみる。
class Bicycle attr_reader :style, :size, :tape_color, :front_shock, :rear_shock def initialize(args) @style = args[:style] @size = args[:size] @tape_color = args[:tape_color] @front_shock = args[:front__shock] @rear_shock = args[:rear_shock] end def spares if style == :road { chain: '10-speed', tare_size: '23', # milimeters tape_color: tape_color } else { chain: '10-speed', tare_size: '2.1', # inches rear_shock: rear_shock } end #... end bike = Bicycle.new(style: :mountain, size: 'S', front_shock: 'Manitou', rear_shock: 'Fox') bike.size # => 'S' bike.spares # => { chain: "10-speed", # tare_size: "2.1", # rear_shock: "Fox" }
- sparesメソッドは有害な影響を保有している。まずstyleが増えた際if文を増やさなくてはならない。また想定外のstyleが指定された場合elseの設定になってしまう。
- if文で評価の対象としているstyle変数は実質的にクラスを2種類の別のものに分けている。大部分は似ている2つのものを1つのクラスにまとめてしまっている。
- 上記問題は継承を使用することで解決する。BicycleクラスをMountainBikeクラスのスーパークラスと定義する。
- 以下はMountainBikeクラスを作る際の起こしがちなミスの例である。
class MountainBike < Bicycle attr_reader :front_shock, :rear_shock def initialize(args) @front_shock = args[:front_shock] @rear_shock = args[:rear_shock] super(args) end def spares super.merge(rear_shock: rear_shock) end end
mountain_bike = MountainBikeike.new(size: 'S', front_shock: 'Manitou', rear_shock: 'Fox') mountain_bike.size # = > 'S' mountain_bike.spares # => { chain: "10-speed", # tare_size: "23", <- 間違い # tape_color: nil, <- 不適切 # rear_shock: "Fox" }
- Bicycleクラスを抽象的なクラスにしてBicycleクラスを継承したロードバイククラスを作るべきである。Bicycleクラスのような抽象的なクラスのことを抽象クラスと呼ぶ。抽象クラスはサブクラスがつくられるためだけに存在している。
- 本書ではBicycleクラスをそのままRoadBikeクラスに変更し新しく空のBicycleクラスを作成、抽象的な変数やメソッドをBicycleクラスに移していく順序をとっている。一度サブクラスに降ろしてからスーパークラスに徐々に移動していく方法はリファクタリングにおいて重要な作業になる。継承で起こる多くの失敗は具象から抽象を厳密に分ける作業の難しさから起こるものである。この作業は横着しない方が良い。
- まずはsizeゲッターを使用出来るようにする。
class Bicycle attr_reader :size def initialize(args={}) @size = args[:size] end end class RoadBike < Bicycle attr_reader :tape_color def initialize(args) @tape_color = args[:tape_color] end #... end
road_bike = RoadBike.new(size: 'M', tape_color: 'red') road_bike.size # => "M" mountain_bike = MountainBike.new(size: 'S', front_shock: 'Manitou', rear_shock: 'Fox') mountain_bike.size # => "S"
- sparesメソッドは自転車の種類ごとに異なりそのままBicycleクラスの昇格することは出来ない。
- 一方sparesメソッド内で定義されているchainとtire_sizeは自転車に共通するものでありsize同様ゲッターセッターを定義すべきである。
class Bicycle attr_reader :size, :chain, :tire_size def initialize(args={}) @size = args[:size] @chain = args[:chain] @tire_size = args[:tire_size] end end
- 上記より全てのサブクラスがsize, chain, tire_sizeを理解するようになった。またサブクラスは各変数に固有の値を設定出来るようになった。
- テンプレートメソッドパターンを使用することで変数に初期値を設定することが出来る。メソッドとして初期値を設定する理由はサブクラスがメソッドをオーバーライドすれば初期値を変更出来るからである。
class Bicycle attr_reader :size, :chain, :tire_size def initialize(args={}) @size = args[:size] @chain = args[:chain] || default_chain @tire_size = args[:tire_size] || default_tire_size end def default_chain '10-speed' end end class RoadBike < Bicycle #... def default_tire_size '23' end end class MountainBike < Bicycle #... def default_tire_size '2.1' end end
road_bike = RoadBike.new(size: 'M', tape_color: 'red') road_bike.tire_size # => "23" road_bike.chain # => "10-speed" mountain_bike = MountainBike.new(size: 'S', front_shock: 'Manitou', rear_shock: 'Fox') mountain_bike.tire_size # => "2.1" mountain_bike.chain # => "10-speed"
- Bicycleクラスはサブクラスにdefault_tire_sizeメソッドの実装を必要としています。現状default_tire_sizeメソッドを作成しないと以下のエラーが発生します。
NameError: undefined local variable or method 'default_tire_size'
- もう少し明示的なエラーを出力させてあげるべきである。NotImplementedErrrorは定義はされているが中身が実装されていない関数を呼び出した時に発生する例外。
class Bicycle #... def default_tire_size raise NotImplementedError, "This #{self.class} cannt respond to: " end end
- 上記コードを記載することでエラーメッセージが分かりやすくなる。
NotImplementedError: This NewBike cannot respond to: 'default_tire_size'
- sparesメソッドを作成します。サブクラスで固有に定義されるものはmergeメソッドを用いて実装する。定義した全コードを以下に示す。
class Bicycle attr_reader :size, :chain, :tire_size def initialize(args={}) @size = args[:size] @chain = args[:chain] || default_chain @tire_size = args[:tire_size] || default_tire_size end def spares { tire_size: tire_size, chain: chain } end def default_chain '10-speed' end def default_tire_size raise NotImplementedError, "This #{self.class} cannt respond to: " end end class RoadBike < Bicycle attr_reader :tape_color def initialize(args) @tape_color = args[:tape_color] super(args) end def spares super.merge({ tape_color: tape_color }) end def default_tire_size '23' end end class MountainBike < Bicycle attr_reader :front_shock, :rear_shock def initialize(args) @front_shock = args[:front_shock] @rear_shock = args[:rear_shock] super(args) end def spares super.merge({ rear_shock: rear_shock }) end def default_tire_size '2.1' end end
- 上記コードのままでも動くがまだ依存がある。サブクラスがsuperを各所で書くことが強要されており新たなサブクラスを作成する際superの記載を忘れると気づきにくいバグになる。
- この問題はフックメッセージを使用することで解決出来る。superを送るように求めるのではなくスーパークラスが代わりに「フック」メッセージを送るようにすることが出来る。
class Bicycle attr_reader :size, :chain, :tire_size def initialize(args={}) @size = args[:size] @chain = args[:chain] || default_chain @tire_size = args[:tire_size] || default_tire_size post_initialize(args) end def post_initialize(args) nil end #... end class RoadBike < Bicycle attr_reader :tape_color def post_initialize(args) @tape_color = args[:tape_color] end #... end
- サブクラスのinitializeを削除することでスーパークラスのinitializeが呼び出される。スーパークラスのinitializeはpost_initializeメソッドを呼び出している。このメソッドでサブクラス固有の変数を定義すれば先程の問題は解決する。
- この変更によりRoadBikeはBicycleに対する知識(size, chain, tire_size)が減った。つまりオブジェクト間の結合度が減ったことになる。
- sparesメソッドもまたフックメッセージを使用出来る。
class Bicycle #... def spares { tire_size: tire_size, chain: chain }.merge(local_spares) end def local_spares nil end #... end class RoadBike < Bicycle #... def local_spares { tape_color: tape_color } end #... end
- 最終的な全コードは以下のようになる。サブクラスが簡潔になりより専門に特化したもののみ持つようになった。
class Bicycle attr_reader :size, :chain, :tire_size def initialize(args={}) @size = args[:size] @chain = args[:chain] || default_chain @tire_size = args[:tire_size] || default_tire_size post_initialize(args) end def spares { tire_size: tire_size, chain: chain }.merge(local_spares) end def post_initialize(args) nil end def local_spares nil end def default_chain '10-speed' end def default_tire_size raise NotImplementedError, "This #{self.class} cannt respond to: " end end class RoadBike < Bicycle attr_reader :tape_color def post_initialize(args) @tape_color = args[:tape_color] end def local_spares { tape_color: tape_color } end def default_tire_size '23' end end class MountainBike < Bicycle attr_reader :front_shock, :rear_shock def post_initialize(args) @front_shock = args[:front_shock] @rear_shock = args[:rear_shock] end def local_spares { rear_shock: rear_shock } end def default_tire_size '2.1' end end
Ruby on Railsチュートリアル 2章
- scaffoldジェネレータを使用してToyアプリケーションを作っていきます。
2.1 アプリケーションの計画
- まずはRailsアプリケーションを新規作成します。
$ cd ~/environment $ rails _6.0.3_ new toy_app $ cd toy_app/
- Gemfileの内容をチュートリアル指定のものへ変更します。
- bundle installします。1章と同じエラーが出るので同様に対処していきます。
$ bundle install --without production ... If you are updating multiple gems in your Gemfile at once, try passing them all to `bundle update` $ bundle config build.puma --with-cflags="-Wno-error=implicit-function-declaration" $ bundle update --without production $ bundle install --without production
- ローカルリポジトリを作成し、コミットします。
$ git init $ git add -A $ git commit -m "Initialize repository"
- ローカルリポジトリの設定をしてプッシュします。
$ git remote add origin https://github.com/***/***.git $ git push -u origin master
- 1.3.4同様にルートにHello, world!を表示するよう変更を加えます。
# /app/controllers/application_controller.rb class ApplicationController < ActionController::Base render html: "Hello,world!!" end
# /config/routes.rb Rails.application.routes.draw do root "application#hello" end
- herokuへデプロイします。
$ heroku create Creating app... done, ⬢ *** https://***.herokuapp.com/ | https://git.heroku.com/***.git $ git remote -v # リモートの設定を確認 $ git add -A $ git commit -m "Add hello" $ heroku create $ git push && git push heroku master
- ちゃんとデプロイ出来てるか確認します。実際のサイトは以下のURLです。
2.2 Usersリソース
- scaffoldコマンドを用いてCRUDの機能が付いたUsersリソースを作成します。
$ rails generate scaffold User name:string email:string Running via Spring preloader in process 39425 invoke active_record create db/migrate/20201017091657_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml invoke resource_route route resources :users invoke scaffold_controller create app/controllers/users_controller.rb invoke erb create app/views/users create app/views/users/index.html.erb create app/views/users/edit.html.erb create app/views/users/show.html.erb create app/views/users/new.html.erb create app/views/users/_form.html.erb invoke test_unit create test/controllers/users_controller_test.rb create test/system/users_test.rb invoke helper create app/helpers/users_helper.rb invoke test_unit invoke jbuilder create app/views/users/index.json.jbuilder create app/views/users/show.json.jbuilder create app/views/users/_user.json.jbuilder invoke assets invoke scss create app/assets/stylesheets/users.scss invoke scss create app/assets/stylesheets/scaffolds.scss
- ログを見るとマイグレーション作成からモデル作成、ルートの作成、コントローラ作成、ビュー作成まで自動でやってくれていますね。scaffold便利。
- モデルデータを作成するためにDBのマイグレーションを行います。
$ rails db:migrate
- rails routesでルーティングを確認してみます。しっかり作成されてますね。
- http://127.0.0.1:3000/users/ にアクセスしてCRUDの動作確認を行います。
演習
1. CSSを知っている読者へ: 新しいユーザーを作成し、ブラウザのHTMLインスペクター機能を使って「User was successfully created.」の箇所を調べてみてください。ブラウザをリロードすると、その箇所はどうなるでしょうか?
- リロードするとpタグの中身が消える。
/* 作成時 */ <p id="notice">User was successfully created.</p> /* リロード後 */ <p id="notice"></p>
2. emailを入力せず、名前だけを入力しようとした場合、どうなるでしょうか?
- 作成出来る。バリデーションしてないから。
3. 「@example.com」のような間違ったメールアドレスを入力して更新しようとした場合、どうなるでしょうか?
- 作成出来る。こちらもバリデーションしてないから。
4. 上記の演習で作成したユーザーを削除してみてください。ユーザーを削除したとき、Railsはどんなメッセージを表示するでしょうか?
<p id="notice">User was successfully destroyed.</p>
Started DELETE "/users/5" for 127.0.0.1 at 2020-10-17 18:33:20 +0900 Processing by UsersController#destroy as HTML Parameters: {"authenticity_token"=>"haACzUla3pcJCjB38uj1z1FtCJt9Nl5cIm0vQV10VvFjVVJHg/pmciQv7jjsSJ+ugYAr10B98DKJ1FAtP7NHjQ==", "id"=>"5"} User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 5], ["LIMIT", 1]] ↳ app/controllers/users_controller.rb:67:in `set_user' (0.0ms) begin transaction ↳ app/controllers/users_controller.rb:57:in `destroy' User Destroy (0.4ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 5]] ↳ app/controllers/users_controller.rb:57:in `destroy' (3.8ms) commit transaction ↳ app/controllers/users_controller.rb:57:in `destroy' Redirected to http://127.0.0.1:3000/users Completed 302 Found in 8ms (ActiveRecord: 4.4ms | Allocations: 2765) Started GET "/users" for 127.0.0.1 at 2020-10-17 18:33:20 +0900 Processing by UsersController#index as HTML Rendering users/index.html.erb within layouts/application User Load (0.1ms) SELECT "users".* FROM "users" ↳ app/views/users/index.html.erb:15 Rendered users/index.html.erb within layouts/application (Duration: 1.6ms | Allocations: 1142) Completed 200 OK in 9ms (Views: 8.8ms | ActiveRecord: 0.1ms | Allocations: 6866)
2.2.2 MVCの挙動
- ルートURLで表示するアクションをUsersコントローラのindexアクションに変更します。
# /config/routes.rb Rails.application.routes.draw do root "users#index" end
演習
1. 図 2.11を参考にしながら、/users/1/edit というURLにアクセスしたときの振る舞いについて図を書いてみてください。
- editアクション内に何も記載が内がよく見るとbefore_actionでeditアクションを実行する前にset_userメソッドを実行している。set_userメソッドではデータベースからユーザ情報を取得している。
# /app/controllers/user_controller.rb class UsersController < ApplicationController before_action :set_user, only: [:show, :edit, :update, :destroy] #... def edit end #... private def set_user @user = User.find(params[:id]) end #... end
2. 図示した振る舞いを見ながら、Scaffoldで生成されたコードの中でデータベースからユーザー情報を取得しているコードを探してみてください。Hint: set_userという特殊な場所の中にあります。
- 1の回答で示した通りUsersコントローラ内のset_userメソッド内で取得している。
3. ユーザーの情報を編集するページのファイル名は何でしょうか?
- 図に示した通りedit.html.erb。/app/views/users/ディレクトリ内にある。
2.3 Micropostsリソース
2.3.1 マイクロソフトを探検する
- Usersリソース同様にscaffoldを使用してMicropostsリソースを作成する。
$ rails generate scaffold Micropost content:text user_id:integer
- データベースの更新を行う。
$ rails db:migrate
- ルーティングを見るとしっかりMicroposts関連が追加されてます。
$ rails routes
- サーバを起動し動作確認を行う。
演習
1. CSSを知っている読者へ: 新しいマイクロポストを作成し、ブラウザのHTMLインスペクター機能を使って「Micropost was successfully created.」の箇所を調べてみてください。ブラウザをリロードすると、その箇所はどうなるでしょうか?
- リロードするとpタグの中身が消える。
/* 作成時 */ <p id="notice">Micropost was successfully created.</p> /* リロード後 */ <p id="notice"></p>
2. マイクロポストの作成画面で、ContentもUserも空にして作成しようとするどうなるでしょうか?
- 作成できる。
3. 141文字以上の文字列をContentに入力した状態で、マイクロポストを作成しようとするとどうなるでしょうか?(ヒント: WikipediaのRubyの記事にある設計思想の引用文が140文字を超えているので、これをコピペしてみましょう)
- 作成できる。
4. 上記の演習で作成したマイクロポストを削除してみましょう。
- 削除しておきます。
2.3.2 マイクロポストをマイクロにする
- Micropostモデルにバリデーションを追加して文字数を140字以内に制限します。
# /app/Models/micropost.rb class Micropost < ApplicationRecord validates :content, length: { maximum: 140 } end
演習
1. 先ほど2.3.1.1の演習でやったように、もう一度Contentに141文字以上を入力してみましょう。どのように振る舞いが変わったでしょうか?
- 140文字以上の登録を使用とするとエラーが発生するようになりました。
2. CSSを知っている読者へ: ブラウザのHTMLインスペクター機能を使って、表示されたエラーメッセージを調べてみてください。
- div#error_explanationタグ内に記載されている。
2.3.2 ユーザーはたくさんマイクロポストを持っている
- MicropostモデルとUserモデルを関連付けさせる。
# /app/Models/user.rb class User < ApplicationRecord has_many :microposts end
# /app/Models/micropost.rb class Micropost < ApplicationRecord belongs_to :user validates :content, length: { maximum: 140 } end
演習
1. ユーザーのshowページを編集し、ユーザーの最初のマイクロポストを表示してみましょう。同ファイル内の他のコードから文法を推測してみてください(コラム 1.2で紹介した技術の出番です)。うまく表示できたかどうか、/users/1 にアクセスして確認してみましょう。
- 以下のコードを追記する。
# /app/views/users/show.html.erb /* ... */ <p> <strong>First Micropost:</strong> <%= @user.microposts.first.content %> </p> /* ... */
- 表示できました。
2. リスト 2.18は、マイクロポストのContentが存在しているかどうかを検証するバリデーションです。マイクロポストが空でないことを検証できているかどうか、実際に試してみましょう(図 2.17のようになっていると成功です)。
- 省略
3. リスト 2.19のFILL_INとなっている箇所を書き換えて、Userモデルのnameとemailが存在していることを検証してみてください(図 2.18)。
# リスト2.19 class User < ApplicationRecord has_many :microposts validates FILL_IN, presence: true validates FILL_IN, presence: true end
- 以下のように書き換えます。
# /app/models/user.rb class User < ApplicationRecord has_many :microposts validates :name, presence: true validates :email, presence: true end
- 動作確認します。
2.3.4 継承の階層
演習
1. Applicationコントローラのファイルを開き、ApplicationControllerがActionController::Baseを継承している部分のコードを探してみてください。
- 以下のコードが該当箇所。
# /app/controllers/application_controller.rb class ApplicationController < ActionController::Base # <- def hello render html: "Hello,world!!" end end
2. ApplicationRecordがActiveRecord::Baseを継承しているコードはどこにあるでしょうか? 先ほどの演習を参考に、探してみてください。ヒント: コントローラと本質的には同じ仕組みなので、app/modelsディレクトリ内にあるファイルを調べてみると...?)
- 以下のコードが該当箇所。
# /app/models/application_record.rb class ApplicationRecord < ActiveRecord::Base # <- self.abstract_class = true end
2.3.5 アプリケーションをデプロイする
- ローカルリポジトリにプッシュします。
$ git add -A $ git commit -m "Finish toy app" $ git push
- herokuにデプロイします。
$ git push heroku
- heroku上でマイグレーションを実行します。
$ heroku run rails db:migrate
演習
- 動作確認だけのため飛ばします。
- 以上で2章終了です。