自分がRailsチュートリアルなどを一通りこなし、実務でRailsを書き始めてようやく学べたRails上でのコーディングのパターンをまとめてみたいと思います。 RailsTutorialを何周かやってみた、これから自分のアプリケーションを書くぞ、という方を想定読者においています。
MVCの責任分割について
RailsはMVCにしたがってコーディングをしていく必要があるのですが、何がモデルで、何がコントローラーなのかというのは結構あいまいだったりします。 たとえば以下のような場合は、MVCがきちんと守れていない可能性があります。
- コントローラー
- actionにトランザクションが書かれている
- ifが3回以上ネストされて書かれている
- ViewHelperがincludeされている
- モデル
- 表示のためにしか使わないようなメソッド(page_title等)がモデルに定義されてしまっている
また、MVCがきちんと守れている場合でも以下のような症状に悩まされている場合があるかもしれません。
- コントローラー
- 独自で定義されているactionが多い(users#update_profileみたいなメソッド)
- モデル
- コード行数が1000行以上ある
- モデルのなかで、複数のモデルにまたがるトランザクションを書いている
このようなパターンに対してどうコーディングするのが良いか書きたいと思います
コントローラー
とにかく薄いコントローラーを書く
結論からいうとRailsにおけるコントローラーの役割は
- ユーザーのリクエストが不正でないかのチェック
- レスポンスコードを返す
ことに尽きます。 ビジネスロジックについては基本的にコントローラーに書く必要性がありません。これらはモデルやサービスクラスなどに書きましょう。 上記のルールに則ると、ほとんどのコントローラーはscaffoldで作成されるような非常にシンプルなactionだけで構成されるはずです。
シンプルな例では以下のようなものになります
1def create2 @user = User.build(user_params)34 if @user.save5 # 正常系の処理(render / redirect)6 else7 # 異常系(render / redirect)8 end9end10
やや込みいったものでも以下になります。
1def create2 @user = ...3 @items = ...45 # 任意の処理を実行する.6 if HandleHardThingWorkflow.new(@user, @items).run7 # 正常系の処理(render / redirect)8 else9 # 異常系(render / redirect)10 end11end12
HandleHardThingWorkflow
は、ビジネスロジックを外部に切り出したクラスです。
initializeで、その処理に必要な引数をすべて取り、runでタスクを実行、true, falseで成功したかどうかを返します(詳細は後述)。
上記のように、コントローラーはタスク処理を実行するだけ、その結果のハンドリングするだけしか行っていません。 このように書くことでどんな複雑な処理であったとしてもコントローラーは同じようなシンプルな見た目になります。
RESTなコントローラーを徹底する
RESTを意識しながらコーディングすることで、:index, :show, :new, :create, :edit, :update, :destroy
といった基本的なaction以外使わずに済みます。
1# app/controllers/users_controller.rb2class UsersController < ApplicationController3 def update_profile4 ...5 end67 def update_password8 ...9 end10end11
たとえば上記のようなコントローラーは、UserProfileController
と、UserPasswordController
に分割することが可能です。
こうすると、それぞれの更新系はupdate
アクションで対応することができます。
コントローラーにincludeするのはビューヘルパーではなくConcernにする
コントローラーの中にhelperがincludeされていたりしませんか?
railsにおけるhelperはViewHelperなので基本的に、Viewの中で利用するためのレイヤーです。
よく、SessionHelperというものなかに、current_user
やsigned_in?
などが定義されており、これがApplicationControllerの中にincludeされていることがあります。
ヘルパーがコントローラーにincludeされていると、ViewのためにViewHelperが更新されたときに意図せずコントローラーに影響が入ってしまいます。 基本的に、Viewがコントローラーに依存することがあっても逆はあってはいけません。なので別のやり方があるならば、避けたいところです。
その点でRailsにはConcernという仕組みがあり、コントローラーで共通で利用するような機能をmoduleとしてincludeすることができます。
また、コントローラーでは,helper_method
というメソッドを使うことができ、これを使えばコントローラー内で定義したメソッドを、viewのなかでも参照することができるようになります。
1# app/controllers/concerns/user_session_module.rb2module UserSessionModule3 extend ActiveSupport::Concern45 included do6 helper_method :signed_in?, :current_user78 def signed_in?9 ...10 end1112 def current_user13 ...14 end1516 ...17 end18end19
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モジュールを使えば自分で実装することもできます。
1# app/decorators/article_decorator.rb2class ArticleDecorator < Draper::Decorator3 delegate_all45 def publication_status6 if published?7 "Published at #{published_at}"8 else9 "Unpublished"10 end11 end1213 def published_at14 object.published_at.strftime("%A, %B %e")15 end16end17
ViewModel
第二の手段としてはViewModel層を作成するというものです。ここでいうViewModelはプレーンなRuby Objectです。
1# app/view_models/user_collection.rb2class UserCollection3 attr_reader :users45 def initialize(users)6 @users = users.to_a7 end89 def names10 @users.map(&:name).join(', ')11 end12end13
使い分けの基準
使い分けは以下を基準にしてもよいかもしれません。
デコレーターを使う時はこんなとき
- 扱うレコードが1つ
- レコードの表示のために本質的に重要であり、複数のビューで参照されうる
- レコードのインターフェースをそのまま維持したい
ViewModelを使う時はこんなとき
- 扱うレコードが複数(コレクションなど)
- レコードのインターフェースを維持しなくても大丈夫(デレゲーション不要、またはダックタイピングなどで十分)
トランザクションスクリプトの抽出
ビジネスロジックをモデルに書いていくと一つ一つの処理が長くなったり、1つのモデルの中で複数のモデルに関する処理を実施しているような場合、これをトランザクションスクリプトとして切り出すことがありえます。 これについては、サービスクラスとか言われることがあるかと思うのですが、トランザクションスクリプトであることを強調する意味合いでworkflowsというディレクトリに切って実装するのが好みです。 workflowsについては、Take my moneyという本に影響されています。
クラス命名は、メソッド同様に動詞から始まるような名前にするとわかりやすいです。
たとえば、記事を作成するならば、create_article_workflow
や、edit_article_workflow
など。
1# app/workflows/create_article_workflow.rb2class CreateArticleWorkflow3 attr_reader :article45 def initialize(title, content, author) # 引数はキーワード引数の場合や、options = {}でHashとして受け取る場合もあります。6 @title = title7 @content = content8 @author = author9 end1011 def run12 @article = Article.new(title: title, content: content, author: author)13 if @article.save14 @article.notify(author.followers)15 true16 else17 false18 end19 end20end21
これをCreateArticleWorkflow.new(title, content, author).run
などと実行します。
先述のとおり、runの返り値はbooleanです。また、成功したときや失敗した時、CreateArticleWorkflow#article
でオブジェクト自体を取得することができるので、ほとんどのパターンでこれを適用することができます。
まとめ
Railsは誰でも簡単にWebアプリケーションを書けるので非常に便利なのですが、長期で保守する必要がでてくるときちんと設計・リファクタリングする必要があります。 もしみなさんの参考になれば幸いです。