17
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ソニックガーデン プログラマAdvent Calendar 2024

Day 1

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

Last updated at Posted at 2024-11-30

この記事はソニックガーデン プログラマ アドベントカレンダーの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 モジュールは以下です。
短いですが includedclass_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 の強力さだと思います。

あとはこれを ApplicationControllerinclude するだけで、全コントローラーで 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 です。
お楽しみに!

17
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?