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

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

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

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

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

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

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

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

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

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

そんな時に役立ちます。
<br>

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

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

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

例えば、以下のようにモデルの`save`メソッドの前後に処理を追加するのに使われます。
```ruby: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`する必要があります。
```ruby
include ActiveModel::Model
include ActiveModel::Callbacks
```

### コールバックを追加したいメソッドを指定
コールバックを追加したいメソッドを、`define_model_callbacks`を使って指定します。
```ruby
define_model_callbacks :save
```
実際のメソッド内では、`run_callbacks`を追加します。
```ruby
def save
  run_callbacks(:save) do
    # 実際の保存処理
    puts "ユーザーを保存中..."
  end
end
```

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

```ruby
before_save :normalize_name
after_save :send_confirmation_email
```

## 2. initializeの前後に処理を追加する
通常、`initialize`メソッドはインスタンスの初期化時に実行されます。

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

```ruby:Product.rb
class Product
  def initialize(attributes = {})
    # superの前にデフォルト値を設定する
    @name ||= "デフォルト名"
    @price ||= 0
    puts "デフォルトの値を設定"
    super
    # super後にログを出力する
    puts "商品は#{@name}, #{@price}で設定されました。"
  end
end
```
ただこの書き方だと、`initialize`メソッドが肥大化して可読性が下がってしまうのが微妙なところです。

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

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

```ruby: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`クラスがあったとします。

```ruby: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`クラスの`initialize`で`super`を呼びます。
```Child.rb
class Child < Parent
  def initialize(attributes = {})
    @name ||= "デフォルト氏名"
    puts "デフォルト氏名を設定"
    super
    puts "氏名：#{@name}"
  end
end

child = Child.new(name: "太郎")
# デフォルト氏名を設定
# 氏名：太郎
```
しかし、コールバックを指定すると`Child`クラス側で`initilaze`をオーバーライドする必要がなくなります。



```ruby: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`を利用することで、次のようなメリットがあります。
<br>

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


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

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

### 活用例
- 保存処理の前後にデータ加工や通知メール送信を追加。
- 初期化処理にデフォルト値の設定やログ出力を挿入。
- 継承クラスで親クラスを上書きせずに独自処理を追加。
<br>
特定のメソッドの前後に処理を付け足したい場面は多いと思います。


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