今回は、Ruby on Rails(以降、Rails)で、誰もが頭を悩ませている、ある問題を1つ解決しましょう。
それは、
・ModelとViewのどっちに書くの問題
です。
非常に簡単な例で、Modelでは日時を”2018-07-12 18:12:34”で保持していますが、Viewでは”2018/7/12”と表示する場合を考えましょう。
この日時のフォーマットを変更するコードは、ModelとViewのどっちに書くのでしょう。
私は、ModelとViewの間にDecoratorを1つ追加して、Decoratorにコードを書くことをおすすめします!
どういうこと?と思ったあなたは、この記事を読むべきです。
それでは、始めましょう。
Decoratorとは
ちょっと概念的な話から始めますので、まずは結論から。
Model→Viewの関係でコードを書いていたところにDecoratorを追加して、Model→Decorator→Viewの関係でコードを書くことをおすすめします。
ここでは、「Decoratorを追加する」と、どうして「ModelとViewのどっちに書くの問題」が解決できるのかを、説明します。
私が書く記事でたびたび登場している以下の表をご覧ください。
コンポーネント(構成要素) | 説明 | 備考 |
---|---|---|
Model(モデル) | データベースを取り扱う | データベースへの格納方法は、Modelに隠ぺいする |
View(ビュー) | 画面表示を取り扱う | 表示方法は、Viewに隠ぺいする |
Controller(コントローラー) | (ユーザーの入力を受けて)ModelとViewにアレコレ指示する | データベースへの格納方法や表示方法は知らない |
これを見ると、日時のフォーマットを変更するコードは、ModelにもViewにも書ける気がします。
Modelを見ると「データベースへの格納方法は、Modelに隠ぺいする」と書かれています。
データベースに格納する”2018-07-12 18:12:34”という形式は、Viewには知られたくないということですから、Viewで日付が必要になったらModel側で適切なフォーマットにするべきという考え方ができます。
一方、Viewを見ると「表示方法は、Viewに隠ぺいする」と書かれています。
”2018/7/12”という表示方法はView側に書くべきとも考えられます。
どちらも正しい気もしますが、どちらも正しくない気がします。
まさに、頭を悩ませるModelとViewのどっちに書くの問題ですね。
私は、Decoratorをおすすめする立場ですので、次のように考えています。
Modelは「データ管理だけ」を担当し、Viewは「情報表示だけ」を担当する、という役割分担です。
そして新しく登場するDecoratorには、「(プログラムで管理しやすい)データ」を「(ユーザーが理解しやすい)情報」に変換する役割を持たせます。
そして、先ほどの日時のフォーマットを変更するコードはDecoratorに書き、ViewではDecoratorで用意したメソッドを利用します。
ModelとViewの間にDecoratorを追加して、データを情報に変換する役割を持たせれば、ModelとViewのどっちに書くの問題」が解決できそうですね!
Draperとは
ここからは、RailsでDecoratorを実現する方法を紹介していきましょう。
この記事では、Railsらしくgemをインストールして、Decoratorを実現します。
Decoratorを簡単に導入するためのgemはいくつかあります。
The Ruby Toolboxでは、「Rails Presenters」というカテゴリーで集約されています。
参考:https://www.ruby-toolbox.com/categories/rails_presenters
今回は、最もダウンロード数が多いDraperを使ってDecoratorを導入してみます。
Draper以外にも、Cells、ActiveDecorator、Apotomo、display_caseなどが登録されていますね。
実装方法や使いかたが異なりますので、最高のgemを探す!という方は試してみると良いでしょう。
動作を理解するためのWebアプリを作成する
DraperによるDecoratorを理解するために、RailsをインストールしてWebアプリを作りましょう。
(1)Railsをインストールします。
私は、以下の記事を参考に、VirtualBoxで作成した仮想パソコンにインストールしたLinux Mintに、Railsの開発環境を作成しました。
基本的には記事の手順に従って操作しますが、app/samurai/sample1ディレクトリを作成する代わりに、app/samurai/decorator-demoディレクトリを作成しました。
Railsを起動して、ブラウザで画面が表示されることを確認したら、いったんRailsを終了してから次に進みます。
Linux Mintのインストールについては、以下の記事で詳しく説明しています。
Draperをインストールする
Railsのインストールが済んでいれば、Draperのインストールは簡単です。
(1)Gemfileの最終行に以下の内容を追加します。
gem 'draper'
(2)以下のコマンドを1行ずつ順番に入力します。
bundle install
Draperがインストールされました。
簡単なDecoratorを使ってみる
簡単な例ですが、Modelで保持している”2018-07-12 18:12:34”という日時を、Viewでは”2018/7/12”と表示するDecoratorを作ってみましょう。
まずは、bin/rails generate scaffoldコマンドです。
(1)「端末」で以下のコマンドを1行ずつ順番に入力します。
bin/rails generate scaffold User name:string
実行結果:
Running via Spring preloader in process 20314 invoke active_record create db/migrate/20180713061008_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml invoke resource_route route resources :users invoke scaffold_controller create app/controllers/users_controller.rb invoke erb create app/views/users create app/views/users/index.html.erb create app/views/users/edit.html.erb create app/views/users/show.html.erb create app/views/users/new.html.erb create app/views/users/_form.html.erb invoke test_unit create test/controllers/users_controller_test.rb invoke helper create app/helpers/users_helper.rb invoke test_unit invoke decorator create app/decorators/user_decorator.rb invoke test_unit create test/decorators/user_decorator_test.rb invoke jbuilder create app/views/users/index.json.jbuilder create app/views/users/show.json.jbuilder create app/views/users/_user.json.jbuilder invoke test_unit create test/system/users_test.rb invoke assets invoke coffee create app/assets/javascripts/users.coffee invoke scss create app/assets/stylesheets/users.scss invoke scss create app/assets/stylesheets/scaffolds.scss
通常のscaffoldで作成されるディレクトリ/ファイルに加えて、以下の4つが作成されています。
invoke decorator create app/decorators/user_decorator.rb invoke test_unit create test/decorators/user_decorator_test.rb
Decoratorらしいディレクトリとファイルですね。
(2)以下のコマンドを1行ずつ順番に入力します。
bin/rails db:migrate bin/rails console
(3)以下のコードを入力します。
User.create(name:"山田太郎") User.create(name:"長瀬来") User.create(name:"立川裕美") User.create(name:"前田達郎") User.create(name:"細川修二") User.create(name:"木村拓磨") exit
(4)app/views/users/index.html.erbを変更します。
変更前:
<%= notice %>
Users
Name | |||
---|---|---|---|
<%= user.name %> | <%= link_to 'Show', user %> | <%= link_to 'Edit', edit_user_path(user) %> | <%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %> |
<%= link_to 'New User', new_user_path %>
変更後:
<%= notice %>
Users
Name | Created at | |||
---|---|---|---|---|
<%= user.name %> | <%= user.created_at %> | <%= link_to 'Show', user %> | <%= link_to 'Edit', edit_user_path(user) %> | <%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %> |
<%= link_to 'New User', new_user_path %>
(5)app/views/users/show.html.erbを変更します。
「Created at」を表示するようにしました。
変更前:
<%= notice %>
Name: <%= @user.name %>
<%= link_to 'Edit', edit_user_path(@user) %> | <%= link_to 'Back', users_path %>
変更後:
<%= notice %>
Name: <%= @user.name %>
Created at: <%= @user.created_at %>
<%= link_to 'Edit', edit_user_path(@user) %> | <%= link_to 'Back', users_path %>
(6)以下のコマンドを入力します。
bin/rails server
Userの一覧をブラウザで確認してみましょう。
(5)ブラウザで「http://localhost:3000/users」にアクセスします。
「Created at」が表示されていますね。
ここでいずれかの「show」をクリックすると、やはり「Created at」が表示されていることが確認できます。
ここからは「Created at」の表示を「2018/7/13」に変更するために、Decoratorを作ります。
Controllerを変更する
まずは、ControllerからViewに対して、通常のオブジェクトを渡す代わりに、Decorator付きのオブジェクトを渡すように変更します。
(1)app/controllers/users_controller.rbを変更します。
変更前:
class UsersController < ApplicationController before_action :set_user, only: [:show, :edit, :update, :destroy] # GET /users # GET /users.json def index @users = User.all end # GET /users/1 # GET /users/1.json def show end (省略)
変更後:
class UsersController < ApplicationController before_action :set_user, only: [:show, :edit, :update, :destroy] # GET /users # GET /users.json def index @users = UserDecorator.decorate_collection(User.all) end # GET /users/1 # GET /users/1.json def show @user = User.find(params[:id]).decorate end (省略)
「http://localhost:3000/users」や「http://localhost:3000/users/1」にアクセスしたときに、Decoratorで機能を追加した(追加する予定の)@usersまたは@userを取得するようにしています。
先ほどと同じURLにアクセスしてみましょう。
(2)ブラウザで「http://localhost:3000/users」にアクセスします。
何も変わっていないことを確認してください。
無駄なことをしたのか!?と思うかもしれませんが、そうではありません。
ここでは、@usersをDecoratorで機能を追加した(追加する予定の)データに差し替えたにもかかわらず、何も変わらずにWebアプリが動作していることが重要です。
Decoratorを変更する
次に、Decorator(app/decorators/user_decorator.rb)を変更して、def created_atを定義し、機能を変更しましょう。
このように、インスタンス変数と同じ名前のメソッドを定義すると、ViewやModelを変更することなく、Decoratorを利用できます。
(1)app/decorators/user_decorator.rbを変更します。
変更前:
class UserDecorator < Draper::Decorator delegate_all # Define presentation-specific methods here. Helpers are accessed through # `helpers` (aka `h`). You can override attributes, for example: # # def created_at # helpers.content_tag :span, class: 'time' do # object.created_at.strftime("%a %m/%d/%y") # end # end end
変更後:
class UserDecorator < Draper::Decorator delegate_all # Define presentation-specific methods here. Helpers are accessed through # `helpers` (aka `h`). You can override attributes, for example: # # def created_at # helpers.content_tag :span, class: 'time' do # object.created_at.strftime("%a %m/%d/%y") # end # end def created_at object.created_at.strftime("%Y/%-m/%-d") end end
(2)ブラウザで「http://localhost:3000/users」にアクセスします。
Decoratorのdef created_atが呼び出され、日付のフォーマットが変わっていますね。
(3)「山田太郎」の「show」をクリックして、http://localhost:3000/users/1にアクセスします。
こちらでもDecoratorのdef created_atが呼び出され、日付のフォーマットが変わっています。
ModelとViewを変更しない
ここまで、ControllerとDecoratorを変更してきました。
一方、ModelとViewは変更していません。
それでも、表示を変更できたということは、初めに話題にしたModelとViewのどっちに書くの問題は、どっちにも書かない(Decoratorに書く)という結論に至ったことになりますね。
まとめ
今回は、DraperによるDecoratorの実装方法を簡単に紹介しました。
Decoratorを使うもっとも大きなメリットは、Modelを変更しなくても、Viewを変更しなくても、表示を変更できるという点です。
今回のようにcreated_atという、インスタンス変数と同じ名前のメソッドを定義するのもポイントでした。
このように、Decoraterで機能を追加したオブジェクトと、機能を追加する前のオブジェクトを、Viewで区別する必要がないように、Decoratorをうまく定義する、というのも、Decoratorパターンの大事な考えかたですので、覚えておきましょう。
それでは、また。