エンタープライズ領域での採用も増えてきたRuby on Railsを使ってWebアプリケーションを作るための入門連載。最新版の4に対応しています。今回は、サンプルプロジェクトをMVCごとにRailsアプリの設計を見直してリファクタリングすることで、これまでの連載のおさらいをします。
前回の『「設定より規約」のRailsで必要なセッティングの基礎知識と国際化/多言語対応』まで、サンプルプロジェクトの「book_library」を題材にRailsのさまざまな機能を紹介してきましたが、今回はRailsアプリケーション開発を紹介してきた本連載のおさらいとして、サンプルプロジェクトをMVCごとにリファクタリングしたいと思います。
「book_library」は社内の書籍を管理するためのアプリケーションで、これまでRailsの各機能を紹介するため場当たり的にさまざまな機能を盛り込んできましたが、もっとシンプルに作ってみましょう。
まずは、連載第7回の「Rails開発を面白くするアクションコントローラーの5大機能とルーティングの基本」で解説したMVCの「C」、コントローラーです。コントローラーの見直しは名前空間やネストによる外部構造と、アクションの定義などの内部構造に分けて進めていきます。
コントローラーの外部構造は「ルーティング」により定義されますが、それに従って「app/controllers」以下のファイルの配置も決まります。現在の「config/routes.rb」は次のコードのようになっていますが、注意して変更していきます。
BookLibrary::Application.routes.draw do root 'books#index' resources :books resources :users do get 'booking', on: :collection post 'message', on: :member resources :books, only: %i(index) end namespace :admin do resources :books, only: %i(index) end get 'admin' => 'admin/books#index' get 'admin/:id' => 'admin/books#show' post 'admin' => 'admin/books#create' end
まず2行目の「root」の定義ですが、これはこのままでもよいでしょう。
ですが、次の「resources :books」と「namespace :admin」のブロック内の「resources :books」はいずれも「books」のリソースを取り扱うルーティングです。しかし、本来であれば管理側でリソースの登録や更新、削除などをしたいところです。
一方、4行目の「resouces :users」はユーザー自身にユーザー登録や更新する操作をさせたいので、このままとしておきます。しかし、このブロック内部の「get」や「post」は必要な処理かもしれませんが、「resources :books」は「index」アクションだけでそれほど重要ではないかもしれません。
最後の「get」や「post」によるルーティングは連載中の説明のために追加しましたが、見るからに違和感がありますね。「namespace :admin」のルーティングで同じアクションを扱えるので削除してしまってもいいでしょう。
以上の内容を踏まえてルーティングを再定義すると、次のようになりました。ちなみに「namespace :admin」のブロック中で「root」を定義すると、「/admin」のルーティングを設定できます。
BookLibrary::Application.routes.draw do # ユーザーサイド root 'books#index' resources :books, only: %w(index show) resources :users do get 'booking', on: :collection post 'message', on: :member end # 管理者サイド namespace :admin do root 'books#index' resources :books end end
前節のルーティングの再定義により、コントローラーごとにアクションを見直す必要があります。まず「BooksController(app/controllers/books_controller.rb)」から取り掛かりましょう。
このコントローラーは連載初期に「scaffold」により生成し、そのまま使ってきました。ですが、「new」「create」「edit」「update」「destroy」のアクションは「Admin::BooksController(app/controllers/admin/books_controller.rb)」に移動させたいと思います。対象の部分を切り取って、フィルターのオプションなどを整理すると「BooksController」は次のようになります。
class BooksController < ApplicationController before_action :set_book, only: [:show] def index @query = Query.new(params[:query]) if @query.valid? @books = Book.where("title like :keyword OR author like :keyword", keyword: @query.keyword) else @books = Book.all end end def show end private def set_book @book = Book.find(params[:id]) end end
切り取った部分は「Admin::BooksController」で再利用しますが、リダイレクト先やビューのリンクなどが「admin」名前空間の中でないため、適宜修正する必要があります。修正した内容については「book_library」の「11」ディレクトリのコードを参照してください。
また、ビューに関しては削除したアクションへのリンクなど不必要なものを取り除きます。「index」アクションのビューを次に示します。
h1 = t('.title') = form_for @query, url: books_path, method: :get do |f| = f.text_field :keyword = f.submit '検索' table thead tr th Title th Author th Outline tbody - @books.each do |book| tr td = link_to book.title, book td = book.author td = book.outline
ここまででも「books#index」のリファクタリングができましたが、次の変更に備えてスペック(spec)を追加しておくのも良い考えです。「spec/controllers/books_controller_spec.rb」に次のように「index」アクションなどのテストを追加します(テストについては連載代9回の「RailsテストフレームワークRSpecのインストールと基本的な使い方、基礎文法」を参照してください)。
require 'rails_helper' RSpec.describe BooksController, :type => :controller do describe "index" do # @queryにQueryのインスタンスが代入される it "assigns a query as @query" do get :index, {}, {} expect(assigns(:query)).to an_instance_of Query end # クエリがないとき、@booksに全てのBookが代入される it "assigns all books as @books without query" do b1 = Book.create!(title: 'hoge') b2 = Book.create!(title: 'fuga') get :index, {}, {} expect(assigns(:books)).to eq [b1, b2] end # @booksにクエリで要求されたBookが代入される it "assigns books as @books required by query" do b1 = Book.create!(title: 'hoge') b2 = Book.create!(title: 'fuga') get :index, {query: {keyword: 'hoge'}}, {} expect(assigns(:books)).to eq [b1] end end …… end
Copyright © ITmedia, Inc. All Rights Reserved.