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

公開日: 2017年12月8日GitHub

自分が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 create
2 @user = User.build(user_params)
3
4 if @user.save
5 # 正常系の処理(render / redirect)
6 else
7 # 異常系(render / redirect)
8 end
9end
10

やや込みいったものでも以下になります。

1def create
2 @user = ...
3 @items = ...
4
5 # 任意の処理を実行する.
6 if HandleHardThingWorkflow.new(@user, @items).run
7 # 正常系の処理(render / redirect)
8 else
9 # 異常系(render / redirect)
10 end
11end
12

HandleHardThingWorkflowは、ビジネスロジックを外部に切り出したクラスです。 initializeで、その処理に必要な引数をすべて取り、runでタスクを実行、true, falseで成功したかどうかを返します(詳細は後述)。

上記のように、コントローラーはタスク処理を実行するだけ、その結果のハンドリングするだけしか行っていません。 このように書くことでどんな複雑な処理であったとしてもコントローラーは同じようなシンプルな見た目になります。

RESTなコントローラーを徹底する

RESTを意識しながらコーディングすることで、:index, :show, :new, :create, :edit, :update, :destroyといった基本的なaction以外使わずに済みます。

1# app/controllers/users_controller.rb
2class UsersController < ApplicationController
3 def update_profile
4 ...
5 end
6
7 def update_password
8 ...
9 end
10end
11

たとえば上記のようなコントローラーは、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のなかでも参照することができるようになります。

1# app/controllers/concerns/user_session_module.rb
2module UserSessionModule
3 extend ActiveSupport::Concern
4
5 included do
6 helper_method :signed_in?, :current_user
7
8 def signed_in?
9 ...
10 end
11
12 def current_user
13 ...
14 end
15
16 ...
17 end
18end
19

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.rb
2class ArticleDecorator < Draper::Decorator
3 delegate_all
4
5 def publication_status
6 if published?
7 "Published at #{published_at}"
8 else
9 "Unpublished"
10 end
11 end
12
13 def published_at
14 object.published_at.strftime("%A, %B %e")
15 end
16end
17

ViewModel

第二の手段としてはViewModel層を作成するというものです。ここでいうViewModelはプレーンなRuby Objectです。

1# app/view_models/user_collection.rb
2class UserCollection
3 attr_reader :users
4
5 def initialize(users)
6 @users = users.to_a
7 end
8
9 def names
10 @users.map(&:name).join(', ')
11 end
12end
13

使い分けの基準

使い分けは以下を基準にしてもよいかもしれません。

デコレーターを使う時はこんなとき

  • 扱うレコードが1つ
  • レコードの表示のために本質的に重要であり、複数のビューで参照されうる
  • レコードのインターフェースをそのまま維持したい

ViewModelを使う時はこんなとき

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

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

ビジネスロジックをモデルに書いていくと一つ一つの処理が長くなったり、1つのモデルの中で複数のモデルに関する処理を実施しているような場合、これをトランザクションスクリプトとして切り出すことがありえます。 これについては、サービスクラスとか言われることがあるかと思うのですが、トランザクションスクリプトであることを強調する意味合いでworkflowsというディレクトリに切って実装するのが好みです。 workflowsについては、Take my moneyという本に影響されています。

クラス命名は、メソッド同様に動詞から始まるような名前にするとわかりやすいです。 たとえば、記事を作成するならば、create_article_workflowや、edit_article_workflowなど。

1# app/workflows/create_article_workflow.rb
2class CreateArticleWorkflow
3 attr_reader :article
4
5 def initialize(title, content, author) # 引数はキーワード引数の場合や、options = {}でHashとして受け取る場合もあります。
6 @title = title
7 @content = content
8 @author = author
9 end
10
11 def run
12 @article = Article.new(title: title, content: content, author: author)
13 if @article.save
14 @article.notify(author.followers)
15 true
16 else
17 false
18 end
19 end
20end
21

これをCreateArticleWorkflow.new(title, content, author).runなどと実行します。 先述のとおり、runの返り値はbooleanです。また、成功したときや失敗した時、CreateArticleWorkflow#articleでオブジェクト自体を取得することができるので、ほとんどのパターンでこれを適用することができます。

まとめ

Railsは誰でも簡単にWebアプリケーションを書けるので非常に便利なのですが、長期で保守する必要がでてくるときちんと設計・リファクタリングする必要があります。 もしみなさんの参考になれば幸いです。

This site uses Google Analytics.
source code