「メソッドの前後に処理を追加したい」
「でもメソッドの内部に処理を書くと、メソッドが肥大化して嫌だ」
「メソッド内で別のメソッドを呼ぶのは、依存関係が生まれて嫌だ」
Railsを使って開発をしている皆さん。
こんな気持ちになることって、あるあるではないでしょうか?
そんなモヤモヤを解決する方法は色々あると思いますが、ActiveModel::Callbacks
を使った解決方法を今回は紹介します。
ActiveModel::Callbacks
は、モデルのライフサイクルイベントに特定の処理を追加できる便利な機能です。
例えば、「保存処理の前後にデータを加工したい」「初期化の際にデフォルト値を設定したい」など、ライフサイクルごとの共通処理を簡潔に追加できます。
「メソッドの前後に処理を足したいけど、メソッド自体は変更したくない」
そんな時に役立ちます。
個人的には、initialize
メソッドの前後に処理を追加したい場合や、子クラスでスーパークラスのinitialize
を上書きせずにカスタマイズしたい場合、柔軟なクラス設計ができるようになるのでおすすめです。
この記事では、ActiveModel::Callbacks
の基礎から、initialize
に対する応用例まで紹介していきます。
1. ActiveModel::Callbacksとは?
ActiveModel::Callbacks
は、クラス内で定義されたライフサイクルイベントの前後にフック処理を挿入するためのモジュールです。
例えば、以下のようにモデルのsave
メソッドの前後に処理を追加するのに使われます。
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_save
とafter_save
を使って指定します。
before_save :normalize_name
after_save :send_confirmation_email
2. initializeの前後に処理を追加する
通常、initialize
メソッドはインスタンスの初期化時に実行されます。
このinitialize
メソッドの前後に処理を追加したい場合、initialize
メソッド内の最初と最後に処理を追加する方法が一般的ではないでしょうか。
class Product
def initialize(attributes = {})
# superの前にデフォルト値を設定する
@name ||= "デフォルト名"
@price ||= 0
puts "デフォルトの値を設定"
super
# super後にログを出力する
puts "商品は#{@name}, #{@price}で設定されました。"
end
end
ただこの書き方だと、initialize
メソッドが肥大化して可読性が下がってしまうのが微妙なところです。
しかしActiveModel::Callbacks
を使うと、initialize
メソッドを修正せずにもっと簡単に処理を挿入できます。
以下の例ではinitialize
の前後に処理を追加しています。
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
クラスがあったとします。
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
クラスのinitialize
でsuper
を呼びます。
class Child < Parent
def initialize(attributes = {})
@name ||= "デフォルト氏名"
puts "デフォルト氏名を設定"
super
puts "氏名:#{@name}"
end
end
child = Child.new(name: "太郎")
# デフォルト氏名を設定
# 氏名:太郎
しかし、コールバックを指定するとChild
クラス側でinitilaze
をオーバーライドする必要がなくなります。
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
を利用することで、次のようなメリットがあります。
-
初期化処理の分離
スーパークラスのinitialize
をオーバーライドせずに、子クラスで独自の処理を簡単に追加可能です。 -
コードの可読性と再利用性の向上
コールバックメソッドを別に定義することで、initialize
や他のメソッドが肥大化するのを防いでくれます。 -
多様なライフサイクルイベントの活用
モデルのsave
やカスタムイベントにも適用でき、様々なユースケースに対応できます。
活用例
- 保存処理の前後にデータ加工や通知メール送信を追加。
- 初期化処理にデフォルト値の設定やログ出力を挿入。
- 継承クラスで親クラスを上書きせずに独自処理を追加。
特定のメソッドの前後に処理を付け足したい場面は多いと思います。
ぜひActiveModel::Callbacks
を使ってみてください!