「メソッドの前後に処理を追加したい」
「でもメソッドの内部に処理を書くと、メソッドが肥大化して嫌だ」
「メソッド内で別のメソッドを呼ぶのは、依存関係が生まれて嫌だ」
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を使ってみてください!