この記事はソニックガーデン プログラマ アドベントカレンダーの1日目の記事です。
トップバッターとして Rails ではポピュラーな機能である concern について書きます。
concern って?おさらい
基本的な使い方については省略しますが、Railsガイドでは以下のように説明されてます。
Railsの「concern(関心事)」とは、大規模なコントローラやモデルの理解や管理を楽にする手法の1つです。
Rails をはじめよう - Railsガイド / 9.3 concernを使う
簡単にいうと Ruby のモジュールにちょっと機能を足したのが concern です。ざっくり処理を共通化する仕組みと言って良いと思います。
手軽な反面、負債にもなりがちで、利用に慎重な方も多いのではないでしょうか。
実際に concern ではなく PORO に委譲した方がクリーンになるパターンも多いのですが、ケースによっては concern によって非常に Rails らしい簡潔な表現で共通化できます。
ここでは私が好きな concern の使い方について説明します。
Rails の宣言的表現を追加する concern
一言で言うと Rails らしい宣言的な表現を追加する concern が好きです。ここでいう Rails らしい宣言的表現とは has_many
とか validates
とか Rails 標準にあるメソッドのことです。
言葉だけでは分かりにくいと思うのでコードで説明します。
例えば、現在表示している画面のメニューをメニュー一覧で強調表示する、という要件を考えます。色々な方法がありますが、私はコントローラーで現在のメニューを宣言的に指定するのが好きす。
class ProductsController < ApplicationController
active_menu :product
#.... index, show など
end
active_menu
いう表現で、 :product
がアクティブなメニューであると宣言しています。
コントローラー冒頭で active_menu :product
と宣言されている様子は before_action
などのように、 Rails コントローラーの標準機能のようにも見えます。
ビュー側はこれに対応した active_menu?
メソッドで現在のメニューを判定できます。
<!-- メニュー -->
<nav>
<ul>
<li class="<%= class_names('active': active_menu?(:product))) %>">商品</li>
<li class="<%= class_names('active': active_menu?(:ranking))) %>">ランキング</li>
<li class="<%= class_names('active': active_menu?(:news))) %>">お知らせ</li>
<li class="<%= class_names('active': active_menu?(:acount))) %>">アカウント</li>
</ul>
</nav>
ビューのコードはコントローラーと対応していており、何が起きるのか予測しやすいコードです。
これを実現する concern モジュールは以下です。
短いですが included
や class_methods
など concern で有用な機能を活用しています。
module MenuActivatable
extend ActiveSupport::Concern
included do
helper_method :active_menu?
end
class_methods do
def active_menu(menu, options = {})
before_action -> { activate_menu(menu) }, options
end
end
def activate_menu(menu)
@active_menu = menu.to_sym
end
def active_menu?(menu)
@active_menu == menu.to_sym
end
end
この concern は MenuActivatable
と言う名前で定義しました。これは最高の名前ではないかも知れませんが、 具体的で責務がしっかり伝わる命名だと思います。
またこのモジュール内の処理は、外部のメソッドや変数に依存してないので、変更を管理しやすいです。
先のコントローラーで宣言した active_menu :product
の部分は単なるクラスメソッドだったことも注目です。クラスメソッドを定義するだけで Rails に新しい語彙を与えられるのは Ruby の強力さだと思います。
あとはこれを ApplicationController
で include
するだけで、全コントローラーで active_menu
の宣言を使えるようになります。
class ApplicationController < ActionController::Base
include MenuActivatable
end
このような concern は共通処理を適切に部品化した上で、 Rails の宣言的表現を増やす形で Rails らしく拡張できる ので私は大好きです。
要は実装の詳細は独立した形で concern に定義して、 include 先で利用を宣言する形です。
もう一つの例: LoggableEnum
もう一つの例を出します。
例えばステータスを管理するカラムがあったとして、変更のタイミングで何らかログに残したいとします。標準の enum
ではそんなことは出来ないので、新たにログ出力する簡易版 enum LoggableEnum
を concern で定義しました。
利用方法はこのようになりました。
class Book < ApplicationRecord
include LoggableEnum
# enum :status, [:draft, :published, :archived]
loggable_enum :status, [:draft, :published, :archived]
end
上にコメントで掲載しましたが、標準の enum
とほぼ同じ宣言を実現できています。
使い方
book = Book.new
book.draft! # => ログ出力: INFO status changed to draft
book.draft? # => true
book.published! # => ログ出力: INFO status changed to published
book.published? # => true
使い勝手も enum
と似たものとなってますが、!
メソッドを呼ぶと保存されると同時にログ出力されるのが違います。
concern は以下のように定義しました。
module LoggableEnum
extend ActiveSupport::Concern
class_methods do
def loggable_enum(field_name, states)
states.map!(&:to_s)
validates field_name, inclusion: { in: states }, allow_nil: true
states.each do |state|
# ! メソッド動的定義
define_method "#{state}!" do
self.assign_attributes(field_name => state)
self.save!
# ログ出力の処理(独自処理の追加部分)
Rails.logger.info("#{field_name} changed to #{state}")
end
# ? メソッド動的定義
define_method "#{state}?" do
self.status == state
end
end
end
end
end
この concern によって enum
のような使い心地をそのままに loggable_enum
を実現できました。宣言部分は enum
ライクな loggable_enum
という標準メソッドがあるかのように見えます。
これを私が別の方法で実現しようとすると、値が固有になったり、DRYでなかったりしてしまいそうです。
他にも 「繰り返し定義してるこの処理、こんな宣言で全部やってくれれば良いのに!」 と思ったら concern で実現できるかも知れません。
まとめ
私の考える concern の注意点と原則、私の好きな concern の書き方について説明しました。
concern は使い方を誤ると手強い負債になりますが、上手く使うことで強力な表現力を追加できる機能をワンセットで提供しています。
この記事で私の考える concern の魅力が伝われば幸いです。
それでは2日目は同じくソニックガーデンの @morikiyo です。
お楽しみに!