大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

【Railsデザインパターン①】Decorator

綺麗なコードを書きたい!!!

こんにちは、大ちゃんの駆け出し技術ブログです。

みなさん、綺麗なコード書きたくないですか??

最近自分はポートフォリオをリリースしまして、リリース後のユーザーの反応から機能などを追加しているのですが、まあコードがぐちゃぐちゃしていて追加が難しい。「これなんのためのコードだっけ?」って思うことは何度もあります。

でも、ふと思いました。

「綺麗なコードってどんなコード?」

自分がコーディングをするたびこれはどこに記述するのがベストなのだろうと考えようとはするのですが、最適解がわからず現場で学んでいくしかないのかなと感じていました。そんなとき、下記の記事を見つけました。

applis.io

Railsデザインパターンとは

Railsデザインパターン。これはどんなことを指しているのでしょうか。

Railsにおけるデザインパターンとは、モデルやコントローラ、ビューに頻出する実装パターンを、オブジェクト設計の原則にもとづいて抽象化したパターンのことです。

ここで出てくるキーワードとして「オブジェクト設計の原則」というものがあります。この言葉の意味はざっくりまとめると下記のようになります。

1つのクラスに1つの役割

https://www.amazon.co.jp/オブジェクト指向設計実践ガイド-Rubyでわかる-進化しつづける柔軟なアプリケーションの育て方-Sandi-Metz/dp/477418361X

つまり、Railsデザインパターンとは、1つのクラスに1つの役割という設計を保つことをRailsのコーディングで実践する方法ということになります。

何故このようなデザインパターンがあるのか。

デザインパターンは、アプリケーションが大きくなるにしたがって起こりがちな設計上の問題を防ぐために必要になります。 代表的な問題がFat ModelやFat Controller、Fat Viewです。Railsはデフォルトでモデルやコントローラ、ビューを用意しています。用意されたこのファイル群だけにコードを記述していくと、オブジェクト指向設計の原則を守るのは難しいです。つまりコードが肥大化してしまいます。 こうなると拡張性や再利用性がなく、またテストも書きづらくなります。これを防ぐために、オブジェクト指向設計の原則に基づいてクラスを分割していく必要があります。

https://applis.io/posts/rails-design-patterns#railsにおけるデザインパターンとは

自分の場合、Fat Controllerが目立ちます。そのため、コードを拡張する時に何度も同じコードを書いている時がたびたびあって、その度に拡張子づらいと思うのです。Railsデザインパターンとはざっくりまとめると

Railsで綺麗なコードを書く頻出の方法」

ということができるでしょう。

今回からいくつかの記事にまたがってRailsの全てのデザインパターンをまとめていきたいと思います。デザインパターンは全部で9つあるそうですが、1つずつ紹介していきます


Decoratorオブジェクト

Decoratorオブジェクトの責務

モデルに対するビューのロジックをカプセル化する

Railsではビュー側で表示する見た目・内容を変更したい場合があります。例えば、以下のように日時を表示するviewファイルがあるとします。start_datetimeはdate型のカラムです。

<p>
  <%= event.start_datetime %><br><br>
</p>

表示:2021-09-09 15:00:00 UTC

お分かりのように非常に表示が見づらい。これを日本標準時でYYYY/MM/DD hh:mmの形式で表示する場合、以下のようになります。

<p>
  <%= event.start_datetime.strftime('%Y/%m/%d %H:%M') %><br><br>
</p>

表示:2021/09/09 15:00

しかし、ビュー側にロジックを書くのはよろしくないとされているため、これをモデルに記述します。

# event.rb
class Event < ApplicationRecord
  def formatted_time
    start_datetime.strftime('%Y/%m/%d %H:%M')
  end
end
<p>
  <%= event.formatted_time %><br><br>
</p>

表示:2021/09/09 15:00

ここでの問題は、viewに表示したいメソッドを追加したけれど、モデルに書くと肥大化してしまうことです。モデルでのメソッドはビューだけでなくコントローラーで使われるメソッドも多く含まれる場合があります。全てのロジックをモデルに移すとFat Modelというモデルにたくさんのコードが書かれてしまう問題が出てきてしまいます。オブジェクト設計の原則である変更しやすいコードから外れてしまいます。

そこで登場するのがDecoratorオブジェクトです。ビューで表示するロジックはモデルではなくDecoratorオブジェクトのファイルに記述する(ロジックのカプセル化)というデザインパターンです。

Decoratorオブジェクトをサポートするgemとして2つのgemがあるそうです。

見たところどちらもメンテナンスは最近されています。しかし、draperの方がスター数が5000に対して、active_decoratorは950程度とかなり差があるため、今回はdraperを使うこととします。

draper

導入

① インストール

Gemfileに記述してインストール

# Gemfile
gem 'draper'
$ bundle install

② コマンドのアクティベーション

rails generate draper:installというコマンドを打つことで次に使用するコマンドを使えるようにします。app/decorators/application_decorator.rbというファイルが作成されます。

$ bundle exec rails generate draper:install

Running via Spring preloader in process 50409
      create  app/decorators/application_decorator.rb

作成されたファイルは以下のようになります。推測できる通り、ApplicationControllerのように他のdecoratorファイルと共通する処理を書くためのファイルです。今回は使用しません。

# app/decorators/application_decorator.rb
class ApplicationDecorator < Draper::Decorator
  # Define methods for all decorated objects.
  # Helpers are accessed through `helpers` (aka `h`). For example:
  #
  #   def percent_amount
  #     h.number_to_percentage object.amount, precision: 2
  #   end
end

③ モデル用のdecorator.rbを生成します。

②で下記コマンドが使えるようになりました。

$ rails generate decorator モデル名

今回はeventモデルを装飾したいため、eventモデルを指定してdecoratorファイルを生成します。

$ bundle exec rails generate decorator event
Running via Spring preloader in process 56951
      create  app/decorators/event_decorator.rb

生成されたファイルは下記になります。

# app/decorators/event_decorator.rb
class EventDecorator < 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

ここでdelegate_allという記述がありますが、これは元のモデルファイルに書かれているロジックがdecoratorファイルでも使用できるようにしています。例えば、先ほど記述したformatted_timeメソッドがモデルファイルevent.rbに記述されていれば、event_decorator.rbでもformatted_timeメソッドが利用できるようになります。ここで新しく同様のメソッドを書く必要がなくなるので、DRYに則っていますね。

④ decoratorファイルにロジックを記述

# app/decorators/event_decorator.rb
class EventDecorator < Draper::Decorator
  delegate_all

  def formatted_time
    object.start_datetime.strftime('%Y/%m/%d %H:%M')
  end
end

ここでいうobjectはモデル自身です。デコレートしているモデルを参照するメソッドと理解しておきます。

ビュー側ではdecorateと明示して以下のように記載します。

<p>
  <%= event.decorate.formatted_time %><br><br>
</p>

表示:2021/09/09 15:00

helperとの違い

ここでhelperメソッドというこれまたビューを装飾するためのファイルがあると思います。これを使えばロジックをビューやモデルに書くことなく、helperに書くことで同様の実装が可能になると思います。

しかし、helperとdecoratorの用途は差別化されています。

helperはモデルから独立し直接関係していない描画ロジックを実装するのに用います。それに対してDecoratorは特定のモデルにがっつり関連した描画ロジックを実装するのに用いるというものです。

今回の場合、eventモデルのstart_datetimeというカラムをガッツリ参照しています。そのため、decoratorを使用することが正しそうです。

反対に、helperには何を書くのかというと、eventに関する関連ページでeventモデルを参照しない描画ロジックを記述すると思います。例えば、event一覧ページがあるとして、そのページに「本日(YYYY/MM/DD)のイベント」と表示させる時に使えます。

# app/helpers/events_helper.rb
module EventsHelper
  def event_at_date
    Date.today.to_time.strftime('%Y/%m/%d')
  end
end

参考記事

Railsデザインパターンまとめ

applis.io

オブジェクト指向設計原則とは

qiita.com

decoratorを導入して、viewの記述をすっきりさせ、modelの肥大化を回避する【Day 3/30 2nd】

note.com

Decoratorの役割とDraperについて

qiita.com