T_Y_CODE

プログラミング学習記録

学習記録 14日目

14日目の学習記録をまとめていきます。

学習計画

学習内容

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
  • Railsではblank?メソッドをRubyに追加している。
演習

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として渡されてきたオブジェクトの種類を調べリードタイムの日数を知る必要がある。

f:id:t_y_code:20201019180710p:plain

  • どのクラスにどの値を用いるかという知識はScheduleクラスに持たせるべきではない。targetに対してlead_daysとして呼び出すことで依存関係を取り除く。targetのクラスがどのクラスなのか気にしていない。ただlead_daysに応答してくれるオブジェクトであればよい。これはダックタイプである。

f:id:t_y_code:20201019180835p:plain

  • オブジェクト指向設計においてオブジェクトは自身の振る舞いは自身で持つべきである。上図はこの規則に反している。targetは自身の変更をしようとしてるにも関わらずScheduleクラスの知識が強制され依存が生じている。
  • 上記からtargetはschedulable?に応答すべきである。以下はBicycleクラスのインスタンスを例にschedulable?メソッドを導入したシーケンス図である。

f:id:t_y_code:20201019182150p:plain

  • 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するだけでロールを担うことが可能になります。

f:id:t_y_code:20201019184641p:plain

  • 抽象スーパークラス内のコードを使わないサブクラスがあってはならない。一部のサブクラスで使うようなコードはスーパークラスに置くべきではない。
  • 継承側でsuperを呼び出すようなコードを書くのは避けるべき。代わりにフックメッセージを使用する。superを送らなければならないようなコードを書くと依存度が増す。
  • フックメッセージは階層構造を浅くしないと使用出来ない。
  • 深い階層構造は自身より上に位置するオブジェクトすべてに依存している。依存をなるべく減らすためには浅い階層構造にすべき。

本日の総括

  • Railsチュートリアル4章は内容がRubyの学習となっているためチェリー本の復習的に読んでいった。演習がいい練習問題になっていた。
  • モジュールの使用方法について学習した。継承との違いを学べ実際にコードを書く際の参考になると感じた。