学習記録 14日目
14日目の学習記録をまとめていきます。
学習計画
- Railsチュートリアル 4章
- オブジェクト指向設計実践ガイド 7章
学習内容
Ruby on Railsチュートリアル 4章
4.1 動機
- 本章のブランチを作成します。
$ git checkout -b rails-flavored-ruby
4.1.2 カスタムヘルパー
- ページタイトルの作成をapplication_helperに分けていきます。
# app/helpers/application_helper.rb module ApplicationHelper def full_title(page_title = '') base_title = "Ruby on Rails Tutorial Sample App" if page_title.empty? base_title else page_title + " | " + base_title end end end
/* app/views/layouts/application.html.erb */ <!DOCTYPE html> <html> <head> <title><%= full_title(yield(:title)) %></title> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> </head> <body> <%= yield %> </body> </html>
- homeページのHomeを消します。まずはテストから修正していきます。修正したらテストが失敗することを確認する。
# test/controllers/static_pages_controller_test.rb require 'test_helper' class StaticPagesControllerTest < ActionDispatch::IntegrationTest #... test "should get home" do get static_pages_home_url assert_response :success assert_select "title", "#{@base_title}" end #... end
$ rails t 5 tests, 9 assertions, 1 failures, 0 errors, 0 skips
- ビューを修正(provide()部の削除)しテストが成功することを確認する。
# app/views/static_pages/home.html.erb <h1>Sample App</h1> <p> This is the home page for the <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a> sample application. </p>
$ rails t 5 tests, 9 assertions, 0 failures, 0 errors, 0 skips
4.2.1 文字列
演習
1. city変数に適当な市区町村を、prefecture変数に適当な都道府県を代入してください。
$ rails c > city = '江戸川区' > prefecture = '東京都' > city # => "江戸川区" > prefecture # => "東京都"
2. 先ほど作った変数と式展開を使って、「東京都 新宿区」のような住所の文字列を作ってみましょう。出力にはputsを使ってください。
> puts "#{prefecture} #{city}" # => 東京都 江戸川区
3. 上記の文字列の間にある半角スペースをタブに置き換えてみてください。(ヒント: 改行文字と同じで、タブも特殊文字です)
- タブは\tですね。
> puts "#{prefecture}\t#{city}" # => 東京都 江戸川区
4. タブに置き換えた文字列を、ダブルクォートからシングルクォートに置き換えてみるとどうなるでしょうか?
> puts '#{prefecture}\t#{city}' # => #{prefecture}\t#{city}
4.2.2 オブジェクトとメッセージ受け渡し
演習
1. "racecar" の文字列の長さはいくつですか? lengthメソッドを使って調べてみてください。
> "racecar".length # => 7
2. reverseメソッドを使って、"racecar"の文字列を逆から読むとどうなるか調べてみてください。
> "racecar".reverse # => "racecar"
3. 変数sに "racecar" を代入してください。その後、比較演算子(==)を使って変数sとs.reverseの値が同じであるかどうか、調べてみてください。
> s = "racecar" > s == s.reverse # => true
4. リスト 4.9を実行すると、どんな結果になるでしょうか? 変数sに "onomatopoeia" という文字列を代入するとどうなるでしょうか?(ヒント: 上矢印(またはCtrl-Pコマンド)を使って以前に使ったコマンドを再利用すると一からコマンドを全部打ち込む必要がなくて便利ですよ。)
> s = "racecar" > puts "It's a palindrome!" if s == s.reverse # => It's a palindrome!
4.2.3 メソッドの定義
演習
1. リスト 4.10のFILL_INの部分を適切なコードに置き換え、回文かどうかをチェックするメソッドを定義してみてください。ヒント: リスト 4.9の比較方法を参考にしてください。
> def palindrome_tester(s) > if s == s.reverse > puts "It's a palindrome!" > else > puts "It's not a palindrome." > end > end
2. 上で定義したメソッドを使って “racecar” と “onomatopoeia” が回文かどうかを確かめてみてください。1つ目は回文である、2つ目は回文でない、という結果になれば成功です。
> palindrome_tester("racecar") # => It's a palindrome! > palindrome_tester("onomatopoeia") # => It's not a palindrome.
3. palindrome_tester("racecar")に対してnil?メソッドを呼び出し、戻り値がnilであるかどうかを確認してみてください(つまりnil?を呼び出した結果がtrueであることを確認してください)。このメソッドチェーンは、nil?メソッドがリスト 4.10の戻り値を受け取り、その結果を返しているという意味になります。
- putsの戻り値はnilのためtrueになる。
> palindrome_tester("racecar").nil? # => It's a palindrome! # => true
4.3.1 配列と範囲演算子
- splitメソッドはデフォルトでは空白文字が設定されている。(今まで.split(' ')してました...)
演習
1. 文字列「A man, a plan, a canal, Panama」を ", " で分割して配列にし、変数aに代入してみてください。
> a = "A man, a plan, a canal, Panama".split(", ") > a # => ["A man", "a plan", "a canal", "Panama"]
2. 今度は、変数aの要素を連結した結果(文字列)を、変数sに代入してみてください。
> s = a.join
> s
# => "A mana plana canalPanama"
3. 変数sを半角スペースで分割した後、もう一度連結して文字列にしてください(ヒント: メソッドチェーンを使うと1行でもできます)。リスト 4.10で使った回文をチェックするメソッドを使って、(現状ではまだ)変数sが回文ではないことを確認してください。downcaseメソッドを使って、s.downcaseは回文であることを確認してください。
> s = s.split.join > s # => "AmanaplanacanalPanama" > palindrome_tester(s) # => It's not a palindrome.
4. aからzまでの範囲オブジェクトを作成し、7番目の要素を取り出してみてください。同様にして、後ろから7番目の要素を取り出してみてください。(ヒント: 範囲オブジェクトを配列に変換するのを忘れないでください)
> ('a'..'z').to_a[6] # => "g" > ('a'..'z').to_a[-7] # => "t"
4.3.2 ブロック
演習
1. 範囲オブジェクト0..16を使って、各要素の2乗を出力してください。
> (0..16).to_a.map { |n| n**2 } # => [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256]
2. yeller(大声で叫ぶ)というメソッドを定義してください。このメソッドは、文字列の要素で構成された配列を受け取り、各要素を連結した後、大文字にして結果を返します。例えばyeller(['o', 'l', 'd'])と実行したとき、"OLD"という結果が返ってくれば成功です。ヒント: mapとupcaseとjoinメソッドを使ってみましょう。
> def yeller(a) > a.map(&:upcase).join > end > yeller(['o', 'l', 'd']) # => OLD
3. random_subdomainというメソッドを定義してください。このメソッドはランダムな8文字を生成し、文字列として返します。ヒント: サブドメインを作るときに使ったRubyコードをメソッド化したものです。
> def random_subdomain > ('a'..'z').to_a.shuffle[0..7].join > end > random_subdomain # => "wucminsb"
4. リスト 4.12の「?」の部分を、それぞれ適切なメソッドに置き換えてみてください。ヒント:split、shuffle、joinメソッドを組み合わせると、メソッドに渡された文字列(引数)をシャッフルさせることができます。
> def string_shuffle(s) > s.split('').shuffle.join > end > string_shuffle("foobar") # => "robaof"
4.3.3 ハッシュとシンボル
演習
1. キーが'one'、'two'、'three'となっていて、それぞれの値が'uno'、'dos'、'tres'となっているハッシュを作ってみてください。その後、ハッシュの各要素をみて、それぞれのキーと値を"'#{key}'のスペイン語は'#{value}'"といった形で出力してみてください。
> spanish = { 'one' => 'uno', 'two' => 'dos', 'three' => 'tres' } > spanish # => {"one"=>"uno", "two"=>"dos", "three"=>"tres"}
2. person1、person2、person3という3つのハッシュを作成し、それぞれのハッシュに:firstと:lastキーを追加し、適当な値(名前など)を入力してください。その後、次のようなparamsというハッシュのハッシュを作ってみてください。1)キーparams[:father]の値にperson1を代入、2)キーparams[:mother]の値にperson2を代入、3)キーparams[:child]の値にperson3を代入。最後に、ハッシュのハッシュを調べていき、正しい値になっているか確かめてみてください。(例えばparams[:father][:first]がperson1[:first]と一致しているか確かめてみてください)
> person1, person2, person3 = {first: 'Foo', last: 'Bar'}, > {first: 'Jon', last: 'Green'}, > {first: 'Emmet', last: 'Brwon'} > params = {} > params[:father] = person1 > params[:mother] = person2 > params # => {:father=>{:first=>"Foo", :last=>"Bar"}, :mother=>{:first=>"Jon", :last=>"Green"}} > params[:father][:first] == person1[:first] # => "true
3. userというハッシュを定義してみてください。このハッシュは3つのキー:name、:email、:password_digestを持っていて、それぞれの値にあなたの名前、あなたのメールアドレス、そして16文字からなるランダムな文字列が代入されています。
> user = { name: "Miyoji Namae", email: "email@mail.com", password_digest: ('a'..'z').to_a.shuffle[0..15].join } > user # => {:name=>"Miyoji Namae", :email=>"email@mail.com", :password_digest=>"pgvrwcstjloazbde"}
4. Ruby API(訳注: もしくはるりまサーチ)を使って、Hashクラスのmergeメソッドについて調べてみてください。次のコードを実行せずに、どのような結果が返ってくるか推測できますか? 推測できたら、実際にコードを実行して推測があっていたか確認してみましょう。
# 予測 { "a" => 100, "b" => 300 } > { "a" => 100, "b" => 200 }.merge({ "b" => 300 }) # => {"a"=>100, "b"=>300}
4.3.4 CSS、再び
- CSSを追加するコードは以下のように捉えられる。
# 元コード stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' # stylesheet_link_tagメソッドの()を省略している stylesheet_link_tag( 'application', media: 'all', 'data-turbolinks-track': 'reload' ) # 最後の引数はハッシュになっている <stylesheet_link_tag( 'application', { media: 'all', 'data-turbolinks-track': 'reload' } )
4.4.1 コンストラクタ
演習
1. 1から10の範囲オブジェクトを生成するリテラルコンストラクタは何でしたか?(復習です)
> (1..10).class # => Range
2. 今度はRangeクラスとnewメソッドを使って、1から10の範囲オブジェクトを作ってみてください。ヒント: newメソッドに2つの引数を渡す必要があります
> ran = Range.new(1, 10) > ran # => 1..10
3. 比較演算子==を使って、上記2つの課題で作ったそれぞれのオブジェクトが同じであることを確認してみてください。
> ran == (1..10) # => true
4.4.2 クラス継承
- Stringクラスを継承したWordクラスを作成し回分か判定するpalindrome?メソッドを作成する。
> class Word < String > def palindrome? > self == self.reverse > end > end > s = Word.new('level') > s.palindrome? # => true > s.length # => 5 > s.class # => Word > s.class.superclass # => String > s.class.superclass.superclass # => Object
演習
1. Rangeクラスの継承階層を調べてみてください。同様にして、HashとSymbolクラスの継承階層も調べてみてください。
> Range.superclass # => Object > Range.superclass.superclass # => BasicObject > Hash.superclass # => Object > Hash.superclass.superclass # => BasicObject > Symbol.superclass # => Object > Symbol.superclass.superclass # => BasicObject
2. リスト 4.15にあるself.reverseのselfを省略し、reverseと書いてもうまく動くことを確認してみてください。
> class Word < String > def palindrome? > self == reverse > end > end > s = Word.new('level') > s.palindrome? # => true
4.4.3 組み込みクラスの変更
- 組み込みクラスのStringにpalindrome?を追加する。
> class String > def palindrome? > self == reverse > end > end > "deified".palindrome? # => true
演習
1. palindrome?メソッドを使って、“racecar”が回文であり、“onomatopoeia”が回文でないことを確認してみてください。南インドの言葉「Malayalam」は回文でしょうか? ヒント: downcaseメソッドで小文字にすることを忘れないで。
> class String > def palindrome? > self == reverse > end > end > "onomatopoeia".palindrome? # => false > "Malayalam".downcase.palindrome? # => true
2. リスト 4.16を参考に、Stringクラスにshuffleメソッドを追加してみてください。ヒント: リスト 4.12も参考になります。
> class String > def shuffle > self.split('').shuffle.join > end > end > "foobar".shuffle # => "boafor"
3. リスト 4.16のコードにおいて、self.を削除してもうまく動くことを確認してください。
> class String > def shuffle > split('').shuffle.join > end > end > "foobar".shuffle # => "rboafo"
4.4.4 コントローラクラス
演習
1. 第2章で作ったToyアプリケーションのディレクトリでRailsコンソールを開き、User.newと実行することでuserオブジェクトが生成できることを確認してみましょう。
$ cd ~/environment/toy_app/ $ rails c > user = User.new > user # => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
2. 生成したuserオブジェクトのクラスの継承階層を調べてみてください。
> user.class.superclass # => ApplicationRecord(abstract) > user.class.superclass.superclass # => ActiveRecord::Base > user.class.superclass.superclass.superclass # => Object > user.class.superclass.superclass.superclass.superclass # => BasicObject
4.4.5 ユーザークラス
- アプリケーションのルートディレクトリに以下のファイルを作成する。
# example.user.rb class User attr_accessor :name, :email def initialize(attributes = {}) @name = attributes[:name] @email = attributes[:email] end def formatted_email "#{@name} <#{@email}>" end end
- railsコンソールを起動し上記クラスを利用してみる。
$ rails c > require './example_user' > example = User.new > example.name # => nil > example.name = "Example User" > example.name # => "Example User" > example.email = "user@example.com" > example.formatted_email # => "Example User <user@example.com>"
- attr_accessorを使用したことでゲッタ, セッタが利用出来るようになっている。
演習
1. Userクラスで定義されているname属性を修正して、first_name属性とlast_name属性に分割してみましょう。また、それらの属性を使って "Michael Hartl" といった文字列を返すfull_nameメソッドを定義してみてください。最後に、formatted_emailメソッドのnameの部分を、full_nameに置き換えてみましょう(元々の結果と同じになっていれば成功です)
- 以下のように変更します。
# example.user.rb class User attr_accessor :first_name, :last_name, :email def initialize(attributes = {}) @first_name = attributes[:first_name] @last_name = attributes[:last_name] @email = attributes[:email] end def formatted_email "#{full_name} <#{@email}>" end def full_name "#{@first_name} #{@last_name}" end end
$ rails c > require './example_user' > example = User.new(first_name: "Michael", last_name: "Hartl", email: "mhartl@example.com") > example.formatted_email # => "Michael Hartl <mhartl@example.com>"
2. "Hartl, Michael" といったフォーマット(苗字と名前がカンマ+半角スペースで区切られている文字列)で返すalphabetical_nameメソッドを定義してみましょう。
# example.user.rb class User #... def alphabetical_name "#{@last_name}, #{@first_name}" end end
3. full_name.splitとalphabetical_name.split(', ').reverseの結果を比較し、同じ結果になるかどうか確認してみましょう。
$ rails c > require './example_user' > example = User.new(first_name: "Michael", last_name: "Hartl", email: "mhartl@example.com") > example.full_name.split # => ["Michael", "Hartl"] > example.alphabetical_name.split(', ').reverse # => ["Michael", "Hartl"]
4.5 最後に
- リモートリポジトリにプッシュする。herokuにもデプロイする。
$ rails t $ git add -A $ git commit -m "Add a full_title helper" $ git push origin rails-flavored-ruby $ git checkout master $ git merge rails-flavored-ruby $ git push origin master $ git push heroku
- 以上で4章は終了です。
オブジェクト指向設計実践ガイド 7章
- アプリケーションを作成していくと以前には関連のなかったオブジェクト同士に共通の振る舞いを持たせなければならない時がある。
- もともと関連のなかったオブジェクト同士が共通のロール(役割)を担うようになるとオブジェクトは互いに関係をもつようになる。
- 多くのオブジェクト指向言語には名前を付けてメソッドのグループを定義する方法が備わっている。この名前付きグループはクラスとは独立しておりどんなオブジェクトにもミックスイン出来る。Rubyではこのような名前付きグループをモジュールと呼ぶ。
- モジュールは様々なクラスのオブジェクトが1ヶ所に定義されたコードを使って共通のロールを担うための完璧な方法である。
- 6章の内容にスケジュールを管理する機能を付けていくことを想定する。
- 各オブジェクトには旅行と旅行の間に休息期間を設ける。自転車が最低1日, 自動車が最低3日, 整備士が最低4日間とする。
- Scheduleクラスが存在すると仮定する。Scheduleクラスのインターフェースには以下の3つのメソッドがあるとする。
- scheduled?(target, starting, ending)
- add(target, starting, ending)
- remove(target, starting, ending)
- Scheduleの責任は、引数として渡されてきたtargetが既にスケジュールされているか確認することと、targetのスケジュールへの追加, 削除を行うこと。
- このクラスは既にスケジュールされたオブジェクトへのスケジュールは出来ないようになっているが休息期間(リードタイム)が考慮されていない。リードタイムを考慮するためにはtargetとして渡されてきたオブジェクトの種類を調べリードタイムの日数を知る必要がある。
- どのクラスにどの値を用いるかという知識はScheduleクラスに持たせるべきではない。targetに対してlead_daysとして呼び出すことで依存関係を取り除く。targetのクラスがどのクラスなのか気にしていない。ただlead_daysに応答してくれるオブジェクトであればよい。これはダックタイプである。
- オブジェクト指向設計においてオブジェクトは自身の振る舞いは自身で持つべきである。上図はこの規則に反している。targetは自身の変更をしようとしてるにも関わらずScheduleクラスの知識が強制され依存が生じている。
- 上記からtargetはschedulable?に応答すべきである。以下はBicycleクラスのインスタンスを例にschedulable?メソッドを導入したシーケンス図である。
- Scheduleクラスは下記のようにコード反映出来る。
class Schedule def scheduled?(schedulable, start_date, end_date) puts "This "#{schedulable.class} " + "is not scheduled\n" + " between #{start_date} and #{end_date}" false end end
- Bicycleクラスのschedulable?メソッドの実装は以下のようになる。
class Bicycle attr_reader :schedule, :size, :chain, :tire_size def initialize(args={}) @schedule = arg[:schedule] || Schedule.new #... end def schedulable?(start_date, end_date) !scheduled?(start_date - lead_days, end_date) end def scheduled?(start_date, end_date) schedule.scheduled?(self, start_date, end_date) end def lead_days 1 end #... end require 'date' starting = Date.parse("2015/09/04") ending = Date.parse("2015/09/10") b = Bicycle.new b.schedulable?(starting, ending) # This Bicycle is not scheduled # between 2015-09-03 and 2015-09-10 # => true
- Scheduleが何者であるか、何をするかをBicycleクラス内に隠している。Bicycleを持つオブジェクトはScheduleの存在や振る舞いを知っておく必要がない。
- 上記例ではBicycleのみがschedulable?メソッドを使用出来るようになりました。MechanicやVehicleもまたこのロールを担うのでModuleを使用して抽象化したメソッドを書いていく。
Module Schedulable attr_reader :schedule def schedule @schedule ||= Schedule.new end def schedulable?(start_date, end_date) !scheduled?(start_date - lead_days, end_date) end def scheduled?(start_date, end_date) schedule.scheduled?(self, start_date, end_date) end # 必要に応じてインクルードする側で置き換える def lead_days 0 end end
- 最初、開始オブジェクトがScheduleクラスに依存していました。その後BicycleクラスがScheduleクラスに依存することで依存の範囲を狭める事が出来ました。今回Scheduableモジュールにその依存を移行することで更に依存の範囲を狭めることが出来たのです。
class Bicycle #... include Schedulable def lead_days 1 end #... end require 'date' starting = Date.parse("2015/09/04") ending = Date.parse("2015/09/10") b = Bicycle.new b.schedulable?(starting, ending) # This Bicycle is not scheduled # between 2015-09-03 and 2015-09-10 # => true
- schedulable?メソッドをモジュールに移動したことで他のオブジェクトは新たにコードを複製することなくincludeするだけでロールを担うことが可能になります。