学習記録 15日目
15日目の学習記録をまとめていきます。
学習計画
- Railsチュートリアル 5章
- オブジェクト指向設計実践ガイド 8章
学習内容
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") %>
- 表示出来ましたね
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が作動しました。
- custom.scssに各要素のレイアウトを整える追記をします。
演習
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") %> -->
- htmlにはコメントアウトされた状態で出力されています。
2. リスト 5.11のコードをcustom.scssに追加し、すべての画像を非表示にしてみてください。うまくいけば、Railsのロゴ画像がHomeページから消えるはずです。先ほどと同様にインスペクタ機能を使って、今度はHTMLのソースコードは残ったままで、画像だけが表示されなくなっていることを確認してみてください。
/* app/assets/stylesheets/custom.scss */ /* ... */ img { display: none; }
- HTMLのソースコードはそのままの状態でimgタグが表示されなくなりました。
- ネコの画像と追記したcssコードを削除しておきます。
5.1.3 パーシャル(partial)
$ 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>
演習
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; } } }
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>
- ちゃんと動作します。
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
- full_titleヘルパーの単体テストを作成する。
$ 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サーバを立ち上げて動作確認をします。
演習
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
- 本番環境での動作確認を行います。
- 以上で5章は終了です。
オブジェクト指向設計実践ガイド 8章
- コンポジションとは組み合わされた全体が単なる部品の集合以上となるように個別の部品を複雑な全体へと組み合わせる行為。例えば自転車はパーツを組み合わせる事で単なるパーツの集合ではなく自転車として機能をする。
- 本書では6章の章末コードからリファクタリングする例を出している。
- Bicycleクラスは現在、継承の階層構造における抽象スーパークラス。ここからコンポジションへと変更したいとする。
- Bicycleクラスをコンポジションとする場合、パーツの集合であるPartsクラスを持つことになる。sparesメソッドはスペアパーツの一覧を保持しておりPartsクラスを作成すればそちらに移行することが出来る。
- 上記よりBicycleであるということはPartsを持つことを意味する。このような関係のことを「has-a」といいクラス図は以下のように示すことができる。
- クラス図には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
- 上記変更によりクラス図は以下のようになりました。
- Partsクラスは部品の集合です。現在各部品はPartsクラスのインターフェース内にありますがPartクラスとすべきです。スペア部品が必要かどうかPartクラスにメッセージを送ることでsparesメソッドは判断が出来るようにします。
- 上記クラス図の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章の後半はとてもためになることが多く書かれているためオブジェクト指向設計で迷った時は読み直したいと思う。