【Rails入門】ViewとModelの間にDecorator(Draper)を置く

今回は、Ruby on Rails(以降、Rails)で、誰もが頭を悩ませている、ある問題を1つ解決しましょう。

それは、

・ModelとViewのどっちに書くの問題

です。

非常に簡単な例で、Modelでは日時を”2018-07-12 18:12:34”で保持していますが、Viewでは”2018/7/12”と表示する場合を考えましょう。

この日時のフォーマットを変更するコードは、ModelViewのどっちに書くのでしょう。

私は、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以外にも、CellsActiveDecoratorApotomodisplay_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

<% @users.each do |user| %> <% end %>
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

<% @users.each do |user| %> <% end %>
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を変更しない

ここまで、ControllerDecoratorを変更してきました。

一方、ModelとViewは変更していません。

それでも、表示を変更できたということは、初めに話題にしたModelとViewのどっちに書くの問題は、どっちにも書かない(Decoratorに書く)という結論に至ったことになりますね。

まとめ

今回は、DraperによるDecoratorの実装方法を簡単に紹介しました。

Decoratorを使うもっとも大きなメリットは、Modelを変更しなくても、Viewを変更しなくても、表示を変更できるという点です。

今回のようにcreated_atという、インスタンス変数と同じ名前のメソッドを定義するのもポイントでした。

このように、Decoraterで機能を追加したオブジェクトと、機能を追加する前のオブジェクトを、Viewで区別する必要がないように、Decoratorをうまく定義する、というのも、Decoratorパターンの大事な考えかたですので、覚えておきましょう。

それでは、また。

この記事を書いた人

侍エンジニア塾は「人生を変えるプログラミング学習」をコンセンプトに、過去多くのフリーランスエンジニアを輩出したプログラミングスクールです。侍テック編集部では技術系コンテンツを中心に有用な情報を発信していきます。

目次