Ruby on Railsアプリケーションを標準のディレクトリ構成の下で開発していくと、次第に肥大化し、開発しづらい状況に陥ってしまいます。 この記事では、モデル、コントローラ、ビューをリファクタリングする各パターンについて解説します。
パターン一覧
この記事では、以下の9つのパターンについて紹介します。 各パターンについて、概要と備考、簡単な実装例を示します。
- Validator
- Callback
- Observer
- Value
- Policy
- Finder
- Service
- Form
- Decorator
なお、HelperやMailer、Jobといった、Rails側で用意されているものについては言及しません。
Validator
Validatorパターンは、モデルのバリデーションをまとめるパターンです。
いくつかの属性で共通する独自のバリデーションや、複雑な条件のバリデーションを切り出すことで、再利用可能にしたり、可読性を向上できます。
app/validators/leave_validator.rb:
class LeaveValidator < ActiveModel::Validator
def validate(record)
events = record.events.where('start_at > ?', Time.now)
if events.exists?
record.errors.add :base, 'You must cancel all events before leave.'
end
end
end
app/models/user.rb:
class User < ActiveRecord::Base
has_many :events
validates_with LeaveValidator
end
Callback
Callbackパターンは、レコードの状態が変化するタイミングで実行する処理をまとめるパターンです。
ここには、モデルの一貫性を保つのに必要な処理をまとめます。
app/callbacks/users/default_name_callback.rb:
class Users::DefaultNameCallback
def before_create(user)
user.name = default_name(user)
end
private
def default_name(user)
user.email.split('@').first
end
end
app/models/user.rb:
class User < ActiveRecord::Base
before_create Users::DefaultNameCallback.new
end
Observer
Observerパターンは、Callbackパターンと同じ働きですが、モデルの一貫性に寄与しない処理をまとめます。
例として、ユーザの投稿に応じて運営側にSlackで通知する、などが考えられます。 Observerをオフにしてもアプリケーションに影響を及ぼしません。
これはrails-observerを利用することで実現できます。
app/observers/comment_observer.rb:
class CommentObserver < ActiveRecord::Observer
def after_create(comment)
CommentMailer.created(comment).deliver_later
end
end
Value
Valueパターンは、値として比較可能なオブジェクトに関するロジックをまとめるパターンです。
以下は、記事(Post)の閲覧数(count)からランクを算出し、また関連するロジックを実装した例です。
app/values/rank.rb:
class Rank
include Comparable
attr_accessor :count
def initialize(count)
@count = count
end
def <=>(other)
@count - other.count
end
def to_s
case @count
when 0..99
'E'
when 100..999
'D'
when 1000..9999
'C'
when 10000..99999
'B'
else
'A'
end
end
end
app/models/post.rb:
class Post < ActiveRecord::Base
def rank
@rank ||= Rank.new(count)
end
end
Policy
Policyパターンは、ユーザに対する認可をまとめるパターンです。
Punditを用いることで、シンプルに実装できます。
app/policies/post_policy.rb:
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
end
def update?
user.admin? && !post.published?
end
end
Finder
Finderパターンは、いくつかのロジックが絡むなどした、複雑な検索をまとめるパターンです。
コントローラでActiveRecordのクエリの記述が長くなった場合など、Finderに委譲することでシンプルに実装できます。
app/finders/posts_finder.rb:
class PostsFinder
def initialize(posts, order, user)
@posts = posts
@order = order
@user = user
end
def posts
case @order
when 'hot'
@posts.hot
when 'recommended'
@posts.recommend_to(@user)
else
@posts
end
end
end
app/controllers/posts_controller.rb:
class PostsController < ApplicationController
def index
...
@posts = PostsFinder.new(@posts, params[:order], current_user).posts
end
end
Service
Serviceパターンは、副作用のある複雑な操作をまとめるパターンです。
コントローラの記述が長い場合、モデルに対する一連の操作をServiceに委譲することでシンプルに実装できます。
app/services/users/omniauth_service.rb:
module Users
class OmniauthService
def initialize(auth)
@auth = auth
end
def find_or_create
user = User.find_or_initialize_by(uid: @auth.uid, provider: @auth.provider)
if user.new_record?
user.assign_attributes(
email: dummy_email,
password: Devise.friendly_token[0, 20]
)
user.skip_confirmation!
user.save
end
user
end
private
def dummy_email
"#{@auth.uid}-#{@auth.provider}@example.com"
end
end
end
Form
Formパターンは、ユーザの入力を検証/整形するロジックをまとめるパターンです。
コンテキストによって異なるバリデーションを適用したり、受け取った入力を永続化するために整形の必要がある場合などに適用します。
app/forms/sign_up_form.rb:
class SignUpForm
include ActiveModel::Model
attr_reader :group_name, :user_name
validates :group_name, presence: true
validates :user_name, presence: true
def save
if valid?
group = Group.find_or_create_by(name: group_name)
group.users.create(name: user_name)
else
false
end
end
end
app/controllers/users_controller.rb:
class UsersController < ApplicationController
def create
@form = SignUpForm.new(params[:sign_up])
if @form.save
...
end
end
end
Decorator
Decoratorパターンは、Viewに表示するための、モデルの状態に応じたロジックをまとめるパターンです。
Helperだとグローバルな名前空間に定義されてしまいますが、Decoratorだと名前空間を汚染せず、またシンプルに実装できます。
ActiveDecoratorを用いると、以下のように記述できます。
module UserDecorator
def full_name
"#{first_name} #{last_name}"
end
end
おわりに
開発当初から、すべてのパターンを適用する必要はないと思います。 実装していく中で、適切なタイミングでパターンの導入を検討しましょう。
Railsアプリケーションのリファクタリングをする際に、参考にしてみてください。