私が好きな 「Rails に宣言的表現を追加する concern」

この記事は[ソニックガーデン プログラマ アドベントカレンダー](https://qiita.com/advent-calendar/2024/sonicgarden)の1日目の記事です。

トップバッターとして Rails ではポピュラーな機能である **concern** について書きます。

## concern って？おさらい

基本的な使い方については省略しますが、Railsガイドでは以下のように説明されてます。

> Railsの「concern（関心事）」とは、大規模なコントローラやモデルの理解や管理を楽にする手法の1つです。
[Rails をはじめよう - Railsガイド / 9.3 concernを使う](https://railsguides.jp/getting_started.html#concern%E3%82%92%E4%BD%BF%E3%81%86)

簡単にいうと Ruby のモジュールにちょっと機能を足したのが concern です。ざっくり処理を共通化する仕組みと言って良いと思います。

手軽な反面、負債にもなりがちで、利用に慎重な方も多いのではないでしょうか。

実際に concern ではなく PORO に委譲した方がクリーンになるパターンも多いのですが、**ケースによっては concern によって非常に Rails らしい簡潔な表現で共通化できます。**

ここでは私が好きな concern の使い方について説明します。

## Rails の宣言的表現を追加する concern


一言で言うと **Rails らしい宣言的な表現を追加する concern** が好きです。ここでいう Rails らしい宣言的表現とは `has_many` とか `validates` とか Rails 標準にあるメソッドのことです。

言葉だけでは分かりにくいと思うのでコードで説明します。

例えば、現在表示している画面のメニューをメニュー一覧で強調表示する、という要件を考えます。色々な方法がありますが、私はコントローラーで現在のメニューを宣言的に指定するのが好きす。

```ruby
class ProductsController < ApplicationController
  active_menu :product

  #.... index, show など
end
```

`active_menu` いう表現で、 `:product` がアクティブなメニューであると宣言しています。
コントローラー冒頭で `active_menu :product` と宣言されている様子は `before_action` などのように、 Rails コントローラーの標準機能のようにも見えます。

ビュー側はこれに対応した `active_menu?` メソッドで現在のメニューを判定できます。

```erb
<!-- メニュー -->
<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?(:account))) %>">アカウント</li>
  </ul>
</nav>
```

ビューのコードはコントローラーと対応していており、何が起きるのか予測しやすいコードです。

これを実現する concern モジュールは以下です。
短いですが `included` や `class_methods` など concern で有用な機能を活用しています。

```ruby
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` の宣言を使えるようになります。

```ruby
class ApplicationController < ActionController::Base
  include MenuActivatable
end
```

このような concern は共通処理を適切に部品化した上で、 **Rails の宣言的表現を増やす形で Rails らしく拡張できる** ので私は大好きです。

要は実装の詳細は独立した形で concern に定義して、 include 先で利用を宣言する形です。

### もう一つの例: LoggableEnum

もう一つの例を出します。

例えばステータスを管理するカラムがあったとして、変更のタイミングで何らかログに残したいとします。標準の `enum` ではそんなことは出来ないので、新たにログ出力する簡易版 enum `LoggableEnum` を concern で定義しました。

利用方法はこのようになりました。

```ruby
class Book < ApplicationRecord
  include LoggableEnum
  
  # enum :status, [:draft, :published, :archived]
  loggable_enum :status, [:draft, :published, :archived]
end
```

上にコメントで掲載しましたが、標準の `enum` とほぼ同じ宣言を実現できています。

使い方

```ruby
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 は以下のように定義しました。

```ruby
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 の魅力が伝われば幸いです。

それでは2日目は同じくソニックガーデンの @morikiyo です。
お楽しみに！