Edited at

実務で学んだRailsの設計・リファクタリング

自分がRailsチュートリアルなどを一通りこなし、実務でRailsを書き始めてようやく学べたRails上でのコーディングのパターンをまとめてみたいと思います。

RailsTutorialを何周かやってみた、これから自分のアプリケーションを書くぞ、という方を想定読者においています。


MVCの責任分割について

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なコントローラーを徹底する

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アクションで対応することができます。


コントローラーにincludeするのはビューヘルパーではなくConcernにする

コントローラーの中にhelperがincludeされていたりしませんか?

railsにおけるhelperはViewHelperなので基本的に、Viewの中で利用するためのレイヤーです。

よく、SessionHelperというものなかに、current_usersigned_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層を作成するというものです。ここでいう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つ

  • レコードの表示のために本質的に重要であり、複数のビューで参照されうる

  • レコードのインターフェースをそのまま維持したい


ViewModelを使う時はこんなとき


  • 扱うレコードが複数(コレクションなど)

  • レコードのインターフェースを維持しなくても大丈夫(デレゲーション不要、またはダックタイピングなどで十分)


トランザクションスクリプトの抽出

ビジネスロジックをモデルに書いていくと一つ一つの処理が長くなったり、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アプリケーションを書けるので非常に便利なのですが、長期で保守する必要がでてくるときちんと設計・リファクタリングする必要があります。

もしみなさんの参考になれば幸いです。