title | tags | author | slide | published_at |
---|---|---|---|---|
実務で学んだRailsの設計・リファクタリング |
Ruby Rails |
wawoon |
false |
2017-12-08 |
export const config = { amp: true }
自分がRailsチュートリアルなどを一通りこなし、実務でRailsを書き始めてようやく学べたRails上でのコーディングのパターンをまとめてみたいと思います。 RailsTutorialを何周かやってみた、これから自分のアプリケーションを書くぞ、という方を想定読者においています。
RailsはMVCにしたがってコーディングをしていく必要があるのですが、何がモデルで、何がコントローラーなのかというのは結構あいまいだったりします。 たとえば以下のような場合は、MVCがきちんと守れていない可能性があります。
- コントローラー
- actionにトランザクションが書かれている
- ifが3回以上ネストされて書かれている
- ViewHelperがincludeされている
- モデル
- 表示のためにしか使わないようなメソッド(page_title等)がモデルに定義されてしまっている
また、MVCがきちんと守れている場合でも以下のような症状に悩まされている場合があるかもしれません。
- コントローラー
- 独自で定義されているactionが多い(users#update_profileみたいなメソッド)
- モデル
- コード行数が1000行以上ある
- モデルのなかで、複数のモデルにまたがるトランザクションを書いている
このようなパターンに対してどうコーディングするのが良いか書きたいと思います
結論からいうとRailsにおけるコントローラーの役割は
- ユーザーのリクエストが不正でないかのチェック
- レスポンスコードを返す
ことに尽きます。 ビジネスロジックについては基本的にコントローラーに書く必要性がありません。これらはモデルやサービスクラスなどに書きましょう。 上記のルールに則ると、ほとんどのコントローラーはscaffoldで作成されるような非常にシンプルなactionだけで構成されるはずです。
シンプルな例では以下のようなものになります
def create
@user = User.build(user_params)
if @user.save
# 正常系の処理(render / redirect)
else
# 異常系(render / redirect)
end
end
やや込みいったものでも以下になります。
def create
@user = ...
@items = ...
# 任意の処理を実行する.
if HandleHardThingWorkflow.new(@user, @items).run
# 正常系の処理(render / redirect)
else
# 異常系(render / redirect)
end
end
HandleHardThingWorkflow
は、ビジネスロジックを外部に切り出したクラスです。
initializeで、その処理に必要な引数をすべて取り、runでタスクを実行、true, falseで成功したかどうかを返します(詳細は後述)。
上記のように、コントローラーはタスク処理を実行するだけ、その結果のハンドリングするだけしか行っていません。 このように書くことでどんな複雑な処理であったとしてもコントローラーは同じようなシンプルな見た目になります。
RESTを意識しながらコーディングすることで、:index, :show, :new, :create, :edit, :update, :destroy
といった基本的なaction以外使わずに済みます。
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def update_profile
...
end
def update_password
...
end
end
たとえば上記のようなコントローラーは、UserProfileController
と、UserPasswordController
に分割することが可能です。
こうすると、それぞれの更新系はupdate
アクションで対応することができます。
コントローラーの中にhelperがincludeされていたりしませんか?
railsにおけるhelperはViewHelperなので基本的に、Viewの中で利用するためのレイヤーです。
よく、SessionHelperというものなかに、current_user
やsigned_in?
などが定義されており、これがApplicationControllerの中にincludeされていることがあります。
ヘルパーがコントローラーにincludeされていると、ViewのためにViewHelperが更新されたときに意図せずコントローラーに影響が入ってしまいます。 基本的に、Viewがコントローラーに依存することがあっても逆はあってはいけません。なので別のやり方があるならば、避けたいところです。
その点でRailsにはConcernという仕組みがあり、コントローラーで共通で利用するような機能をmoduleとしてincludeすることができます。
また、コントローラーでは,helper_method
というメソッドを使うことができ、これを使えばコントローラー内で定義したメソッドを、viewのなかでも参照することができるようになります。
# app/controllers/concerns/user_session_module.rb
module UserSessionModule
extend ActiveSupport::Concern
included do
helper_method :signed_in?, :current_user
def signed_in?
...
end
def current_user
...
end
...
end
end
Concernを利用することでRailsのパターンに反しない形で、helperをコントローラーにincludeするのと同じことが実現できるので、ぜひそうしましょう。
Railsにおけるモデル(ActiveRecord)の役割は、DAO(Data Access Object)と、ビジネスロジックのカプセル化の2つです。 Data Access Objectというのは、データストアの違いをラップして共通のインターフェースでレコードの取得、作成、更新、削除ができるようにしてくれるオブジェクトのことです。 DAOと、サービス層(ビジネスロジックをまとめたもの)を分ける設計もありうるのですが、それらの分割があいまいになりがちなので、一緒くたにしようというのがActiveRecordの設計方針です(たぶん)。
これはRailsの生産性を非常に高めている優れた設計なのですが、一方でビジネスロジックが多くなると、1つのファイル/クラスにコード行数が増えすぎてしまいます。 また、ビューでしか呼ばれないメソッドのような、そもそもモデルの責任範囲でないものまで混ざってしまう、ということが発生します。
ビューに関わるようなメソッドが増えてきたのならば以下のような方法でビューに関するオブジェクトを用意するべきだと思います。
第一の手段としてはデコレーター層を導入するというものです。 デコレーターを利用すると受け取ったメソッドを元のオブジェクトにdelegateしつつ、独自にメソッドを追加することができます。 主な実装方法としてはdraper、または active_decoratorを利用するのが一般的ですが、Ruby標準のdelegateメソッドや、forwardableモジュールを使えば自分で実装することもできます。
# app/decorators/article_decorator.rb
class ArticleDecorator < Draper::Decorator
delegate_all
def publication_status
if published?
"Published at #{published_at}"
else
"Unpublished"
end
end
def published_at
object.published_at.strftime("%A, %B %e")
end
end
第二の手段としてはViewModel層を作成するというものです。ここでいうViewModelはプレーンなRuby Objectです。
# app/view_models/user_collection.rb
class UserCollection
attr_reader :users
def initialize(users)
@users = users.to_a
end
def names
@users.map(&:name).join(', ')
end
end
使い分けは以下を基準にしてもよいかもしれません。
- 扱うレコードが1つ
- レコードの表示のために本質的に重要であり、複数のビューで参照されうる
- レコードのインターフェースをそのまま維持したい
- 扱うレコードが複数(コレクションなど)
- レコードのインターフェースを維持しなくても大丈夫(デレゲーション不要、またはダックタイピングなどで十分)
ビジネスロジックをモデルに書いていくと一つ一つの処理が長くなったり、1つのモデルの中で複数のモデルに関する処理を実施しているような場合、これをトランザクションスクリプトとして切り出すことがありえます。 これについては、サービスクラスとか言われることがあるかと思うのですが、トランザクションスクリプトであることを強調する意味合いでworkflowsというディレクトリに切って実装するのが好みです。 workflowsについては、Take my moneyという本に影響されています。
クラス命名は、メソッド同様に動詞から始まるような名前にするとわかりやすいです。
たとえば、記事を作成するならば、create_article_workflow
や、edit_article_workflow
など。
# app/workflows/create_article_workflow.rb
class CreateArticleWorkflow
attr_reader :article
def initialize(title, content, author) # 引数はキーワード引数の場合や、options = {}でHashとして受け取る場合もあります。
@title = title
@content = content
@author = author
end
def run
@article = Article.new(title: title, content: content, author: author)
if @article.save
@article.notify(author.followers)
true
else
false
end
end
end
これをCreateArticleWorkflow.new(title, content, author).run
などと実行します。
先述のとおり、runの返り値はbooleanです。また、成功したときや失敗した時、CreateArticleWorkflow#article
でオブジェクト自体を取得することができるので、ほとんどのパターンでこれを適用することができます。
Railsは誰でも簡単にWebアプリケーションを書けるので非常に便利なのですが、長期で保守する必要がでてくるときちんと設計・リファクタリングする必要があります。 もしみなさんの参考になれば幸いです。