9
0

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 10

【Rails】ActiveModel::Callbacksでメソッドの前後に処理を追加したい

Posted at

「メソッドの前後に処理を追加したい」

「でもメソッドの内部に処理を書くと、メソッドが肥大化して嫌だ」

「メソッド内で別のメソッドを呼ぶのは、依存関係が生まれて嫌だ」

Railsを使って開発をしている皆さん。

こんな気持ちになることって、あるあるではないでしょうか?

そんなモヤモヤを解決する方法は色々あると思いますが、ActiveModel::Callbacksを使った解決方法を今回は紹介します。

ActiveModel::Callbacksは、モデルのライフサイクルイベントに特定の処理を追加できる便利な機能です。

例えば、「保存処理の前後にデータを加工したい」「初期化の際にデフォルト値を設定したい」など、ライフサイクルごとの共通処理を簡潔に追加できます。

「メソッドの前後に処理を足したいけど、メソッド自体は変更したくない」

そんな時に役立ちます。

個人的には、initializeメソッドの前後に処理を追加したい場合や、子クラスでスーパークラスのinitializeを上書きせずにカスタマイズしたい場合、柔軟なクラス設計ができるようになるのでおすすめです。

この記事では、ActiveModel::Callbacksの基礎から、initializeに対する応用例まで紹介していきます。

1. ActiveModel::Callbacksとは?

ActiveModel::Callbacksは、クラス内で定義されたライフサイクルイベントの前後にフック処理を挿入するためのモジュールです。

例えば、以下のようにモデルのsaveメソッドの前後に処理を追加するのに使われます。

User.rb
class User
  include ActiveModel::Model
  include ActiveModel::Callbacks

  # saveメソッドにコールバックを追加できるよう定義
  define_model_callbacks :save

  # save実行前の処理
  before_save :normalize_name
  # save実行後の処理
  after_save :send_confirmation_email

  def save
    # コールバックを実行しながら保存処理を実行
    run_callbacks(:save) do
      # 実際の保存処理
      puts "ユーザーを保存中..."
    end
  end

  private

  def normalize_name
    puts "ユーザーの氏名を正規化..."
  end

  def send_confirmation_email
    puts "確認メールを送信中..."
  end
end

user = User.new
user.save

# ユーザーの氏名を正規化...
# ユーザーを保存中...
# 確認メールを送信中...

事前準備

ActiveModel::Callbacksを使うためには以下をincludeする必要があります。

include ActiveModel::Model
include ActiveModel::Callbacks

コールバックを追加したいメソッドを指定

コールバックを追加したいメソッドを、define_model_callbacksを使って指定します。

define_model_callbacks :save

実際のメソッド内では、run_callbacksを追加します。

def save
  run_callbacks(:save) do
    # 実際の保存処理
    puts "ユーザーを保存中..."
  end
end

コールバック前後の処理を指定

コールバック前後に追加したい処理を、別のメソッドで記載します。
その上でbefore_saveafter_saveを使って指定します。

before_save :normalize_name
after_save :send_confirmation_email

2. initializeの前後に処理を追加する

通常、initializeメソッドはインスタンスの初期化時に実行されます。

このinitializeメソッドの前後に処理を追加したい場合、initializeメソッド内の最初と最後に処理を追加する方法が一般的ではないでしょうか。

Product.rb
class Product
  def initialize(attributes = {})
    # superの前にデフォルト値を設定する
    @name ||= "デフォルト名"
    @price ||= 0
    puts "デフォルトの値を設定"
    super
    # super後にログを出力する
    puts "商品は#{@name}, #{@price}で設定されました。"
  end
end

ただこの書き方だと、initializeメソッドが肥大化して可読性が下がってしまうのが微妙なところです。

しかしActiveModel::Callbacksを使うと、initializeメソッドを修正せずにもっと簡単に処理を挿入できます。

以下の例ではinitializeの前後に処理を追加しています。

Product.rb
class Product
  include ActiveModel::Model
  include ActiveModel::Callbacks

  define_model_callbacks :initialize

  before_initialize :set_defaults
  after_initialize :log_initialization

  def initialize(attributes = {})
    run_callbacks(:initialize) do
      super
    end
  end

  private

  def set_defaults
    @name ||= "デフォルト名"
    @price ||= 0
    puts "デフォルトの値を設定"
  end

  def log_initialization
    puts "商品は#{@name}, #{@price}で設定されました。"
  end
end

product = Product.new(name: "Book", price: 20)
# デフォルトの値を設定
# 商品は#{@name}, #{@price}で設定されました。

継承時のinitializeの優位性

継承関係がある場合、スーパークラスのinitializeを直接書き換えずに、子クラス独自の初期化処理を追加できるのが、ActiveModel::Callbacksを使う大きなメリットです。

通常のやり方では、子クラスでスーパークラスのinitializeをオーバーライドし、手動でsuperを呼び出す必要があります。

しかし、コールバックを使うことでスーパークラスのロジックをそのまま保ちながら、子クラスで追加の処理を簡単に行うことができます。

例えば、以下のようなParentクラスがあったとします。

Parent.rb
class Parent
  include ActiveModel::Model
  include ActiveModel::Callbacks

  define_model_callbacks :initialize

  before_initialize :set_default_type

  def initialize(attributes = {})
    run_callbacks(:initialize) do
      super
    end
  end
end

このParentクラスを継承してChildクラスを定義します。
この時、Childクラスでは初期化の前後に追加の処理が必要だとします。

本来であればParentクラスのinitializeをオーバーライドして追加の処理を記載します。
その上でChildクラスのinitializesuperを呼びます。

Child.rb
class Child < Parent
  def initialize(attributes = {})
    @name ||= "デフォルト氏名"
    puts "デフォルト氏名を設定"
    super
    puts "氏名:#{@name}"
  end
end

child = Child.new(name: "太郎")
# デフォルト氏名を設定
# 氏名:太郎

しかし、コールバックを指定するとChildクラス側でinitilazeをオーバーライドする必要がなくなります。

Child.rb
class Child < Parent
  before_initialize :set_default_name
  after_initialize :log_initialization

  private

  def set_default_name
    @name ||= "デフォルト氏名"
    puts "デフォルト氏名を設定"
  end

  def log_initialization
    puts "氏名:#{@name}"
  end
end

child = Child.new(name: "太郎")
# デフォルト氏名を設定
# 氏名:太郎

結果として、

  • 初期化はParentクラスの役割
  • 初期化前後の処理はChildクラスの役割

というように、クラス同士で役割の分担ができるようになります。

4. まとめ

ActiveModel::Callbacksを利用することで、次のようなメリットがあります。

  1. 初期化処理の分離
    スーパークラスのinitializeをオーバーライドせずに、子クラスで独自の処理を簡単に追加可能です。

  2. コードの可読性と再利用性の向上
    コールバックメソッドを別に定義することで、initializeや他のメソッドが肥大化するのを防いでくれます。

  3. 多様なライフサイクルイベントの活用
    モデルのsaveやカスタムイベントにも適用でき、様々なユースケースに対応できます。

活用例

  • 保存処理の前後にデータ加工や通知メール送信を追加。
  • 初期化処理にデフォルト値の設定やログ出力を挿入。
  • 継承クラスで親クラスを上書きせずに独自処理を追加。

特定のメソッドの前後に処理を付け足したい場面は多いと思います。

ぜひActiveModel::Callbacksを使ってみてください!

9
0
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
9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?