T_Y_CODE

プログラミング学習記録

学習記録 15日目

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

学習計画

学習内容

Ruby on Railsチュートリアル 5章

5.1 構造を追加する
  • 本章のブランチを作成します。
$ git checkout -b filling-in-layout
5.1.1 ナビオゲーション
  • サイト全体のレイアウトファイルにナビゲーション部を追記する。
  • homeのビューファイルに指定の追記を行う。
演習

1. Webページと言ったらネコ画像、というぐらいにはWebにはネコ画像が溢れていますよね。リスト 5.4のコマンドを使って、図 5.3のネコ画像をダウンロードしてきましょう11 。

$ curl -OL https://cdn.learnenough.com/kitten.jpg

2. mvコマンドを使って、ダウンロードしたkitten.jpgファイルを適切なアセットディレクトリに移動してください(参考: 5.2.1)。

$ mv kitten.jpg app/assets/images/kitten.jpg

3. image_tagを使って、kitten.jpg画像を表示してみてください(図 5.4)。

<!-- app/views/static_pages/home.html.erb -->
<div class="center jumbotron">
  <h1>Welcome to the Sample App</h1>

  <h2>
    This is the home page for the
    <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
    sample application.
  </h2>

  <%= link_to "Sign up now!", '#', class: "btn btn-lg btn-primary" %>
</div>

<%= link_to image_tag("rails.svg", alt: "Rails logo", width: "200px"), "https://rubyonrails.org/" %>
<%= image_tag("kitten.jpg", alt: "kitten") %>
  • 表示出来ましたね

f:id:t_y_code:20201020135342p:plain

5.1.2 BootstrapとカスタムCSS
  • bootstrap-sassを追加します。
  • bundle installします。
$ bundle install --without production
  • 新しくsassファイルを作成する。
$ touch app/assets/stylesheets/custom.scss
  • 作成したsassファイルにbootstrapをインポートする記述を追加する。
/* app/assets/stylesheets/custom.scss */
@import "bootstrap-sprockets";
@import "bootstrap";
  • 無事bootstrapが作動しました。

f:id:t_y_code:20201020140204p:plain

  • custom.scssに各要素のレイアウトを整える追記をします。

f:id:t_y_code:20201020140420p:plain

演習

1. リスト 5.10を参考にして、5.1.1.1で使ったネコ画像をコメントアウトしてみてください。また、ブラウザのHTMLインスペクタ機能を使って、コメントアウトするとHTMLのソースからも消えていることを確認してみてください。

<!-- app/views/static_pages/home.html.erb -->
<div class="center jumbotron">
  <h1>Welcome to the Sample App</h1>

  <h2>
    This is the home page for the
    <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
    sample application.
  </h2>

  <%= link_to "Sign up now!", '#', class: "btn btn-lg btn-primary" %>
</div>

<%= link_to image_tag("rails.svg", alt: "Rails logo", width: "200px"), "https://rubyonrails.org/" %>
<!-- <%= image_tag("kitten.jpg", alt: "kitten") %> -->

f:id:t_y_code:20201020140903p:plain
2. リスト 5.11のコードをcustom.scssに追加し、すべての画像を非表示にしてみてください。うまくいけば、Railsのロゴ画像がHomeページから消えるはずです。先ほどと同様にインスペクタ機能を使って、今度はHTMLのソースコードは残ったままで、画像だけが表示されなくなっていることを確認してみてください。

/* app/assets/stylesheets/custom.scss */
/* ... */
img {
  display: none;
}
  • HTMLのソースコードはそのままの状態でimgタグが表示されなくなりました。

f:id:t_y_code:20201020141153p:plain

  • ネコの画像と追記したcssコードを削除しておきます。
5.1.3 パーシャル(partial)
  • まずはIEHTML5対応部分をパーシャルしていきます。パーシャルファイルはファイル名の先頭が_になる。
$ touch app/views/layouts/_shim.html.erb
<!-- app/views/layouts/_shim.html.erb -->
<!--[if lt IE 9]>
  <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
  </script>
<![endif]-->
<!-- 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' %>
    <%= render 'layouts/shim' %>
  </head>
<!-- ... -->
</html>
  • ヘッダーの内容もパーシャルします。
$ touch app/views/layouts/_header.html.erb
<!-- app/views/layouts/_header.html.erb -->
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", '#', id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", '#' %></li>
        <li><%= link_to "Help", '#' %></li>
        <li><%= link_to "Log in", '#' %></li>
      </ul>
    </nav>
  </div>
</header>
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
  <!-- ... -->
  <body>
    <%= render 'layouts/header'%>
    <div class="container">
      <%= yield %>
    </div>
  </body>
</html>
  • パーシャルを利用してフッターを追加します。
$ touch app/views/layouts/_footer.html.erb
<!-- app/views/layouts/_footer.html.erb -->
<footer class="footer">
  <small>
    The <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
    by <a href="https://www.michaelhartl.com/">Michael Hartl</a>
  </small>
  <nav>
    <ul>
      <li><%= link_to "About",   '#' %></li>
      <li><%= link_to "Contact", '#' %></li>
      <li><a href="https://news.railstutorial.org/">News</a></li>
    </ul>
  </nav>
</footer>
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
  <!-- ... -->
  <body>
    <%= render 'layouts/header'%>
    <div class="container">
      <%= yield %>
      <%= render 'layouts/footer' %>
    </div>
  </body>
</html>
  • custom.scssにフッターレイアウト用のcssを追記する。
  • フッターが追加されました。

f:id:t_y_code:20201020142348p:plain

演習

1. Railsがデフォルトで生成するheadタグの部分を、リスト 5.18のようにrenderに置き換えてみてください。ヒント: 単純に削除してしまうと後でパーシャルを1から書き直す必要が出てくるので、削除する前にどこかに退避しておきましょう。

<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <%= render 'layouts/rails_default'%>
    <%= render 'layouts/shim' %>
  </head>
  <body>
    <%= render 'layouts/header'%>
    <div class="container">
      <%= yield %>
      <%= render 'layouts/footer' %>
    </div>
  </body>
</html>

2. リスト 5.18のようなパーシャルはまだ作っていないので、現時点ではテストは red になっているはずです。実際にテストを実行して確認してみましょう。

$ rails t
5 tests, 0 assertions, 0 failures, 5 errors, 0 skips

3. layoutsディレクトリにheadタグ用のパーシャルを作成し、先ほど退避しておいたコードを書き込み、最後にテストが green に戻ることを確認しましょう。

  • パーシャルファイルを作成します。
$ touch app/views/layouts/_rails_default.html.erb
<!-- app/views/layouts/_rails_default.html.erb -->
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  • テストが成功することを確認します。
$ rails t
5 tests, 9 assertions, 0 failures, 0 errors, 0 skips
5.2.2 素晴らしい構文を備えたスタイルシート
  • Sassは要素のネストが出来る。また変数の使用が出来る。
  • 以下custom.scssをネストでまとめ変数を使用し書き換えたものである。
/* app/assets/stylesheets/custom.scss */
@import "bootstrap-sprockets";
@import "bootstrap";

/* mixins, variables, etc. */

$gray-medium-light: #eaeaea;


/* universal */

body {
  padding-top: 60px;
}

section {
  overflow: auto;
}

textarea {
  resize: vertical;
}

.center {
  text-align: center;
  h1 {
    margin-bottom: 10px;
  }
}

/* typography */

h1, h2, h3, h4, h5, h6 {
  line-height: 1;
}

h1 {
  font-size: 3em;
  letter-spacing: -2px;
  margin-bottom: 30px;
  text-align: center;
}

h2 {
  font-size: 1.2em;
  letter-spacing: -1px;
  margin-bottom: 30px;
  text-align: center;
  font-weight: normal;
  color: $gray-light;
}

p {
  font-size: 1.1em;
  line-height: 1.7em;
}

/* header */

#logo {
  float: left;
  margin-right: 10px;
  font-size: 1.7em;
  color: white;
  text-transform: uppercase;
  letter-spacing: -1px;
  padding-top: 9px;
  font-weight: bold;
  &:hover {
    color: white;
    text-decoration: none;
  }
}

/* footer */

footer {
  margin-top: 45px;
  padding-top: 5px;
  border-top: 1px solid $gray-medium-light;
  color: $gray-light;
  a {
    color: $gray;
    &:hover {
      color: $gray-darker;
    }
  }
  small {
    float: left;
  }
  ul {
    float: right;
    list-style: none;
    li {
      float: left;
      margin-left: 15px;
    }
  }
}
演習

1. 5.2.2で提案したように、footerのCSSを手作業で変換してみましょう。具体的には、リスト 5.17の内容を1つずつ変換していき、リスト 5.20のようにしてみてください。

  • 省略
5.3.1 Contactページ
  • Contactページの追加については3章の演習でやったため省略します。
5.3.2 RailsのルートURL
  • ルーティングを名前付きで定義したい場合以下のように行う。以下の例の場合、GETリクエストが/helpに送信された時StaticPagesコントローラのhelpアクションが実行されるようになる。また、help_path, help_urlといった名前付きルートも使えるようになる。
get  '/help', to: 'static_pages#help'
  • ルーティングを変更し名前付きで定義する。
# config/routes.rb
Rails.application.routes.draw do
  root "static_pages#home"
  get '/help',    to: 'static_pages#help'
  get '/about',   to: 'static_pages#about'
  get '/contact', to: 'static_pages#contact'
end
  • この時点でテストが失敗するようになる。
$ rails t
4 tests, 2 assertions, 0 failures, 3 errors, 0 skips
  • テストの記述を修正する。
# test/contollers/static_pages_controller_test.rb
require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  def setup
    @base_title = "Ruby on Rails Tutorial Sample App"
  end

  test "should get home" do
    get root_url
    assert_response :success
    assert_select "title", "#{@base_title}"
  end

  test "should get help" do
    get help_url
    assert_response :success
    assert_select "title", "Help | #{@base_title}"
  end

  test "should get about" do
    get about_url
    assert_response :success
    assert_select "title", "About | #{@base_title}"
  end

  test "should get contact" do
    get contact_url
    assert_response :success
    assert_select "title", "Contact | #{@base_title}"
  end
end
$ rails t
4 tests, 8 assertions, 0 failures, 0 errors, 0 skips
演習

1. 実は名前付きルートは、as:オプションを使って変更することができます。有名なFar Sideの漫画に倣って、Helpページの名前付きルートをhelfに変更してみてください(リスト 5.29)。

# config/routes.rb
Rails.application.routes.draw do
  root "static_pages#home"
  get '/help',    to: 'static_pages#help', as: 'helf'
  get '/about',   to: 'static_pages#about'
  get '/contact', to: 'static_pages#contact'
end

2. 先ほどの変更により、テストが red になっていることを確認してください。リスト 5.28を参考にルーティングを更新して、テストを green にして見てください。

$ rails t
4 tests, 6 assertions, 0 failures, 1 errors, 0 skips
  • テストを修正します。
# test/contollers/static_pages_controller_test.rb
require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest
  #...

  test "should get help" do
    get helf_url
    assert_response :success
    assert_select "title", "Help | #{@base_title}"
  end

  #...
end
$ rails t
4 tests, 8 assertions, 0 failures, 0 errors, 0 skips

3. エディタのUndo機能を使って、今回の演習で行った変更を元に戻して見てください。

  • 戻しておきます。
5.3.3 名前付きルート
  • 名前付きルートを設定したのでビューに反映していきます。
/* app/views/layouts/_header.html.erb */
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", help_path %></li>
        <li><%= link_to "Log in", '#' %></li>
      </ul>
    </nav>
  </div>
</header>
/* app/views/layouts/_footer.html.erb */
<footer class="footer">
  <small>
    The <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
    by <a href="https://www.michaelhartl.com/">Michael Hartl</a>
  </small>
  <nav>
    <ul>
      <li><%= link_to "About", about_path %></li>
      <li><%= link_to "Contact", contact_path %></li>
      <li><a href="https://news.railstutorial.org/">News</a></li>
    </ul>
  </nav>
</footer>
演習

1. リスト 5.29のようにhelfルーティングを作成し、レイアウトのリンクを更新してみてください。

# config/routes.rb
Rails.application.routes.draw do
  root "static_pages#home"
  get '/help',    to: 'static_pages#help', as: 'helf'
  get '/about',   to: 'static_pages#about'
  get '/contact', to: 'static_pages#contact'
end
/* app/views/layouts/_header.html.erb */
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", helf_path %></li>
        <li><%= link_to "Log in", '#' %></li>
      </ul>
    </nav>
  </div>
</header>
  • ちゃんと動作します。

f:id:t_y_code:20201020150613p:plain
2. 前回の演習と同様に、エディタのUndo機能を使ってこの演習で行った変更を元に戻してみてください。

  • 省略
5.3.4 リンクのテスト
  • 各ページのリンクが適切に貼れているか、個数が合っているかをテスト化します。まずはrails generate integration_testコマンドでテストファイルを作成します。
$ rails g integration_test site_layout
  • 作成されたテストファイルにコードを記述する。
# test/integration/site_layout_test_rb
require 'test_helper'

class SiteLayoutTest < ActionDispatch::IntegrationTest
  
  test "layout links" do
    get root_path
    assert_template 'static_pages/home'
    assert_select "a[href=?]", root_path, count: 2
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
  end
end
/* app/views/layouts/_header.html.erb */
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", helf_path %></li>
        <li><%= link_to "Log in", '#' %></li>
      </ul>
    </nav>
  </div>
</header>
  • テストを行う。統合テストのみ行うには以下のコマンドを使用する。
$ rails test:integration
1 tests, 5 assertions, 0 failures, 0 errors, 0 skips
# 統合テストが成功したら
# すべてのテストを行う。
$ rails t
5 tests, 13 assertions, 0 failures, 0 errors, 0 skips
演習

1. footerパーシャルのabout_pathをcontact_pathに変更してみて、テストが正しくエラーを捕まえてくれるかどうか確認してみてください。

/* app/views/layouts/_footer.html.erb */
<footer class="footer">
  <small>
    The <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
    by <a href="https://www.michaelhartl.com/">Michael Hartl</a>
  </small>
  <nav>
    <ul>
      <li><%= link_to "About", contact_path %></li>
      <li><%= link_to "Contact", contact_path %></li>
      <li><a href="https://news.railstutorial.org/">News</a></li>
    </ul>
  </nav>
</footer>
$ rails test:integration
1 tests, 4 assertions, 1 failures, 0 errors, 0 skips
  • 失敗しましたね。

2. リスト 5.35で示すように、Applicationヘルパーで使っているfull_titleヘルパーを、test環境でも使えるようにすると便利です。こうしておくと、リスト 5.36のようなコードを使って、正しいタイトルをテストすることができます。ただし、これは完璧なテストではありません。例えばベースタイトルに「Ruby on Rails Tutoial」といった誤字があったとしても、このテストでは発見することができないでしょう。この問題を解決するためには、full_titleヘルパーに対するテストを書く必要があります。そこで、Applicationヘルパーをテストするファイルを作成し、リスト 5.37のFILL_INの部分を適切なコードに置き換えてみてください。ヒント: リスト 5.37ではassert_equal <期待される値>, <実際の値>といった形で使っていましたが、内部では==演算子で期待される値と実際の値を比較し、正しいかどうかのテストをしています。

  • 指示通りにtest_helper.rbにApplicationHelperをincludeする記述を追加する。ApplicationHelperはモジュールのためinclude出来る。
  • テストでfull_titleが使用出来るようになる。
# test/integration/site_layout_test_rb
require 'test_helper'

class SiteLayoutTest < ActionDispatch::IntegrationTest
  
  test "layout links" do
    get root_path
    assert_template 'static_pages/home'
    assert_select "a[href=?]", root_path, count: 2
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path

    get contact_path
    assert_select "title", full_title("Contact")
  end
end
$ rails test:integration
1 tests, 6 assertions, 0 failures, 0 errors, 0 skips
$ touch test/helpers/application_helper_test.rb
# test/helpers/application_helper_test.rb
require 'test_helper'

class ApplicationHelperTest < ActionView::TestCase
  test "full title helper" do
    assert_equal full_title, "Ruby on Rails Tutorial Sample App"
    assert_equal full_title("Help"), "Help | Ruby on Rails Tutorial Sample App"
  end
end
  • テストが成功することを確認。
$ rails t
6 tests, 16 assertions, 0 failures, 0 errors, 0 skips
5.4.1 Usersコントローラ
  • Usersコントローラを作成します。newアクションを作成しておきます。
$ rails g controller Users new
      create  app/controllers/users_controller.rb
       route  get 'users/new'
      invoke  erb
      create    app/views/users
      create    app/views/users/new.html.erb
      invoke  test_unit
      create    test/controllers/users_controller_test.rb
      invoke  helper
      create    app/helpers/users_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/users.scss
  • コントローラ, ビュー, テストファイルが作成される。他にもヘルパーやCSSが作成されていますね。自動生成されたテストファイルは成功するよう記載されている。
$ rails t
7 tests, 17 assertions, 0 failures, 0 errors, 0 skips
演習

1. 表 5.1を参考にしながらリスト 5.41を変更し、users_new_urlではなくsignup_pathを使えるようにしてみてください。

  • ルーティングを変更します。
# config/routes.rb
Rails.application.routes.draw do
  root "static_pages#home"
  get '/help',    to: 'static_pages#help'
  get '/about',   to: 'static_pages#about'
  get '/contact', to: 'static_pages#contact'
  get '/signup',  to: 'users#new'
end

2. 先ほどの変更を加えたことにより、テストが red になったことを確認してください。なお、この演習はテスト駆動開発(コラム 3.3)で説明した red / green のリズムを作ることを目的としています。このテストは次の5.4.2で green になるよう修正します。

# test/controllers/user_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest
  test "should get new" do
    get signup_path
    assert_response :success
  end

end
$ rails t
7 tests, 17 assertions, 0 failures, 0 errors, 0 skips
5.4.2 ユーザー登録用URL
  • 作成したsignup_pathへのリンクを追記します。
<!-- app/views/static_pages/home.html.erb -->
<div class="center jumbotron">
  <h1>Welcome to the Sample App</h1>

  <h2>
    This is the home page for the
    <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
    sample application.
  </h2>

  <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
</div>

<%= link_to image_tag("rails.svg", alt: "Rails logo", width: "200px"), "https://rubyonrails.org/" %>
  • signup用ページのビューを編集します。
<!-- app/views/users/new.html.erb -->
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<p>This will be a signup page for new users.</p>
  • railsサーバを立ち上げて動作確認をします。

f:id:t_y_code:20201020154228p:plain

演習

1. もしまだ5.4.1.1の演習に取り掛かっていなければ、まずはリスト 5.41のように変更し、名前付きルートsignup_pathを使えるようにしてください。また、リスト 5.43で名前付きルートが使えるようになったので、現時点でテストが green になっていることを確認してください。

$ rails t
7 tests, 17 assertions, 0 failures, 0 errors, 0 skips

2. 先ほどのテストが正しく動いていることを確認するため、signupルートの部分をコメントアウトし、テスト red になることを確認してください。確認できたら、コメントアウトを解除して green の状態に戻してください。

# config/routes.rb
Rails.application.routes.draw do
  root "static_pages#home"
  get '/help',    to: 'static_pages#help'
  get '/about',   to: 'static_pages#about'
  get '/contact', to: 'static_pages#contact'
  # get '/signup',  to: 'users#new'
end
  • テストが失敗することを確認。
$ rails t
7 tests, 8 assertions, 0 failures, 3 errors, 0 skips

3. リスト 5.32の統合テストにsignupページにアクセスするコードを追加してください(getメソッドを使います)。コードを追加したら実際にテストを実行し、結果が正しいことを確認してください。ヒント: リスト 5.36で紹介したfull_titleヘルパーを使ってみてください。

# test/integration/site_layout_test.rb
require 'test_helper'

class SiteLayoutTest < ActionDispatch::IntegrationTest
  
  test "layout links" do

    #...

    get signup_path
    assert_template 'users/new'
    assert_select "title", full_title("Sign up")
  end
end
$ rails test:integration
1 tests, 8 assertions, 0 failures, 0 errors, 0 skips
$ rails t
7 tests, 19 assertions, 0 failures, 0 errors, 0 skips
5.5 最後に
  • ローカルリポジトリにプッシュしherokuにデプロイします。
$ rails t
$ git add -A
$ git commit -m "Finish layout and routes"
$ git push origin filling-in-layout
$ git checkout master
$ git merge filling-in-layout
$ git push origin master
$ git push heroku
  • 本番環境での動作確認を行います。

f:id:t_y_code:20201020155624p:plain

  • 以上で5章は終了です。

オブジェクト指向設計実践ガイド 8章

  • コンポジションとは組み合わされた全体が単なる部品の集合以上となるように個別の部品を複雑な全体へと組み合わせる行為。例えば自転車はパーツを組み合わせる事で単なるパーツの集合ではなく自転車として機能をする。
  • 本書では6章の章末コードからリファクタリングする例を出している。
  • Bicycleクラスは現在、継承の階層構造における抽象スーパークラス。ここからコンポジションへと変更したいとする。
  • Bicycleクラスをコンポジションとする場合、パーツの集合であるPartsクラスを持つことになる。sparesメソッドはスペアパーツの一覧を保持しておりPartsクラスを作成すればそちらに移行することが出来る。
  • 上記よりBicycleであるということはPartsを持つことを意味する。このような関係のことを「has-a」といいクラス図は以下のように示すことができる。

f:id:t_y_code:20201020191840p:plain

  • クラス図には1が振ってある。これはBicycle1つにつきPartsが1つあるという意味である。
  • 新しいBicycleクラスは以下のようになる。
class Bicycle
  attr_reader :size, :parts

  def initialize(args={})
    @size = args[:size]
    @parts = args[:parts]
  end

  def spares
    parts.spares
  end
end
  • Partsクラスまたサブクラスは以下のようになる。
class Parts
  attr_reader :chain, :tire_size

  def initialize(args={})
    @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 default_tire_size
    raise NotImplementedError
  end

  def post_initialize(args)
    nil
  end

  def local_spares
    {}
  end

  def default_chain
    '10-speed'
  end
end

class RoadBikeParts < Parts
  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 MountainBikeParts < Parts
  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
  • 上記変更によりクラス図は以下のようになりました。

f:id:t_y_code:20201020193245p:plain

  • Partsクラスは部品の集合です。現在各部品はPartsクラスのインターフェース内にありますがPartクラスとすべきです。スペア部品が必要かどうかPartクラスにメッセージを送ることでsparesメソッドは判断が出来るようにします。

f:id:t_y_code:20201020194426p:plain
f:id:t_y_code:20201020194239p:plain

  • 上記クラス図の1..*はPartsクラスはPartオブジェクトを1つ以上持っているという意味。
  • 上記変更によりPartsクラスはより簡潔な内容になりました。
class Bicycle
  attr_reader :size, :parts

  def initialize(args={})
    @size = args[:size]
    @parts = args[:parts]
  end

  def spares
    parts.spares
  end
end

class Parts
  attr_reader :parts

  def initialize(parts)
    @parts = parts
  end

  def spares
    parts.select {|part| part.needs_spare}
  end
end

class Part
  attr_reader :name, :description, :needs_spare

  def initialize(args)
    @name = args[:name]
    @description = args[:description]
    @needs_spare = args.fetch(:needs_spare, true)
  end
end
  • 各部品はPartクラスのオブジェクトとして定義します。
chain = Part.new(name: 'chain', description: '10-speed')

road_tire = Part.new(name: 'tire_size', description: '23')

tape =  Part.new(name: 'tape_color', description: 'red')

mountain_tire = Part.new(name: 'tire_size', description: '2.1')

rear_shock = Part.new(name: 'rear_shock', description: 'Fox')

front_shock = Part.new(name: 'front_shock', description: 'Manitou', needs_spare: false)
  • そして個々のPartオブジェクトはPartsオブジェクトにひとまとめにグループ化が出来ます。グループ化されたPartsオブジェクトからBicycleを作ることが出来ます。
road_bike_parts =  Parts.new([chain, road_tire, tape])
road_bike = Bicycle.new(size: 'L', parts: road_bike_parts)
road_bike.size # => 'L'
road_bike.spares
# => [#<Part:0x00000101036770
#         @name="chain",
#         @description="10-speed",
#         @needs_spere=true>
# => [#<Part:0x0000010102dc60
#         @name="tire-size",
#         @description="23",
#         #...
  • 上記の変更をしても以前のBicycle階層構造とほぼ変わりなく振る舞います。変化があったのはsparesメソッドがPartオブジェクトの配列を返すところです。
  • Partオブジェクトの配列を返していますが決してPartクラスである必要はありません。あくまでPartのロールを担っているオブジェクトでありname, description, needs_spareに応答できるオブジェクトであればいいのです。
  • PartsオブジェクトはPartsクラスであり配列のように扱うことが出来ません。sparesは配列で返すため使用していく内に以下のようなエラーにぶつかるかもしれない。
mountain_bike.spares.size # => 3
mountain_bike.parts.size # => NoMethodError
  • そのうちeachやsort等も使用したくなるかもしれません。Partsを配列のようなものにしてみます。PartsをArrayのサブクラスにします。
class Parts < Array
  def spares
    select {|part| part.needs_spare}
  end
end
  • 上記の変更により新たな問題が出てきます。
combo_parts = (mountain_bike_parts + road_bike.parts)
combo_parts.size # => 7
combo_parts.spares # => NoMethodError

mountain_bike.parts.class # => Parts
road_bike.parts.class # => Parts
combo_parts.class # => Array
  • +が返すオブジェクトはArrayオブジェクトのためsparesメッセージを理解してくれません。
  • Arrayの機能を全て実装すると不備が起きてしまうためsize, eachを@parts配列に委譲しEnumberableをインクルードする。
reqire 'forwardable'
class Parts
  extend Forwardable
  def_delefators :@parts, :size, :each
  include Enumberable

  def initialize(parts)
    @parts = parts
  end

  def spares
    select {|part| part.needs_spare}
  end
end
  • mountain_bikeを作る組み合わせ等を覚えておかなければならないのは大変です。またアプリケーションのあちこちにmountain_bikeを作るコードを書くことになります。他のオブジェクトを作るための情報は1つの場所にまとめるべきです。Partの組み合わせはconfigにまとめます。
road_config =
  [['chain', '10-speed'],
   ['tire_size', '23'],
   ['tape_color', 'red']]
mountain_config =
  [['chain', '10-speed'],
   ['tire_size', '2.1'],
   ['front_shock', 'Manitou', false],
   ['rear_shock', 'Fox']]
  • 他のオブジェクトを製造するオブジェクトのことをファクトリと呼びます。Partsを作成するファクトリを作成します。
module PartsFactory
  def self.build(config, part_class = part, parts_class = parts)
    parts_class.new(
      config.collect do |part_config|
        part_class.new(
          name: part_config[0],
          description: part_config[1],
          needs_spare: part_config.fetch(2, ture)
        )
      end
    )
  end
end
  • PartsFactoryはconfigの構造を知っています。configを書く側からすれば規則通りに書けばPartsFactoryがオブジェクトを作ってくれるようになる。また、PartsFactory経由でオブジェクトが作られることが当然になります。
road_parts = PartsFactory.build(road_config)
# => [#<Part:0x00000101036770
#         @name="chain",
#         @description="10-speed",
#         @needs_spere=true>,
#     #<Part:0x0000010102dc60
#         @name="tire-size",
#         @description="23",
#         #...
  • ここでPartクラスをみてみるとPartsFactoryモジュールと内容が被っていることがわかる。
class Part
  attr_reader :name, :description, :needs_spare

  def initialize(args)
    @name = args[:name]
    @description = args[:description]
    @needs_spare = args.fetch(:needs_spare, true)
  end
end
  • Partクラスを削除してOpenStructクラスを用いてPartsFactoryクラス内で属性をもたせるようにする。
require 'ostruct'
module PartsFactory
  def self.build(config, part_class = part, parts_class = parts)
    parts_class.new(
      config.collect do |part_config|
        create_part(part_config)
      end
    )
  end

  def create_part(part_config)
    OpenStruct.new(
          name: part_config[0],
          description: part_config[1],
          needs_spare: part_config.fetch(2, ture)
      )
  end
end
  • これでPartクラスは不要になりました。
road_parts = PartsFactory.build(road_config)
# => [#<OpenStruct
#         name="chain",
#         description="10-speed",
#         needs_spere=true>,
#     #<OpenStruct
#         name="tire-size",
#         description="23",
#         #...
  • 以上の変更の結果のすべてのコードを以下に示す。新しい種類の自転車を追加するのもたった3行のconfigを書けばいいだけになりました。
class Bicycle
  attr_reader :size, :parts

  def initialize(args={})
    @size = args[:size]
    @parts = args[:parts]
  end

  def spares
    parts.spares
  end
end

require 'forwardable'
class Parts
  extend Forwardable
  def_delegators :@parts, :size, :each
  include Enumerable

  def initialize(parts)
    @parts = parts
  end

  def spares
    parts.select {|part| part.needs_spare}
  end
end

require 'ostruct'
module PartsFactory
  def self.build(config, part_class = part, parts_class = parts)
    parts_class.new(
      config.collect do |part_config|
        create_part(part_config)
      end
    )
  end

  def create_part(part_config)
    OpenStruct.new(
          name: part_config[0],
          description: part_config[1],
          needs_spare: part_config.fetch(2, ture)
      )
  end
end

road_config =
  [ ['chain', '10-speed'],
    ['tire_size', '23'],
    ['tape_color', 'red'] ]
mountain_config =
  [ ['chain', '10-speed'],
    ['tire_size', '2.1'],
    ['front_shock', 'Manitou', false],
    ['rear_shock', 'Fox'] ]

本日の総括

  • Railsチュートリアルは次の章辺りから難しくなってくる記憶があるので気を引き締めていきたい。
  • 継承, モジュール, コンポジションについて学んだ。本書はどのテクニックを適切に使うかは経験が必要であり経験は自身の失敗から学ぶべきと述べられている。これから多くのコードを書いていき失敗し、リファクタリングし適切なコードを最初から書けるようになっていきたいと感じた。8章の後半はとてもためになることが多く書かれているためオブジェクト指向設計で迷った時は読み直したいと思う。