Active Supportのskip_callback動かない問題

## **1. 問題**
`TemporaryArticle`は`Article`からextendしています。
`Article`モデル内に、`push_notification`コールバックを定義しています。

```rb:models/article.rb
class Article
  include Mongoid::Document

  field: name

  has_one :draft_article, dependent: :destroy, class_name: TemporaryArticle
  after_save :push_notification
  
  def push_notification
    #do great work
  end

end
```
```rb:models/temporary_article.rb
class TemporaryArticle < Article
  belongs_to :public_article, class_name: Article.name
  skip_callback :save, :after, :push_notification
end
```


`TemporaryArticle`は`push_notification`を呼びたくないので、`skip_callback`を利用して、`push_notification`を呼ばないようにします。
しかし、`skip_callback`がうまく動かなくて、`TemporaryArticle#save`するとき、 `push_notification`はまだ呼ばれています。

## **2. TL;DR**
解決方法: `TemporaryArticle `定数がある行は各コールバックメソッドの下に置いてください。

```rb:models/article.rb
class Article
  include Mongoid::Document

  field: name

  #move this line to the bellow of callback methods
  #has_one :draft_article, dependent: :destroy, class_name: TemporaryArticle

  after_save :push_notification

  # move to here
  has_one :draft_article, dependent: :destroy, class_name: TemporaryArticle
  
  def push_notification
    #do great work
  end

end
```

## **3. 詳しく説明**
### 3.1. `callback`- How it work?
`callback`のソースコードを見てみましょう。

```rb:rails/activesupport/lib/active_support/callbacks.rb
def set_callback(name, *filter_list, &block)
  type, filters, options = normalize_callback_params(filter_list, block)

  self_chain = get_callbacks name
  mapped = filters.map do |filter|
    Callback.build(self_chain, filter, type, options)
  end

  __update_callbacks(name) do |target, chain|
    options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)
    target.set_callbacks name, chain
  end
end

def skip_callback(name, *filter_list, &block)
  type, filters, options = normalize_callback_params(filter_list, block)

  options[:raise] = true unless options.key?(:raise)

  __update_callbacks(name) do |target, chain|
    filters.each do |filter|
      callback = chain.find { |c| c.matches?(type, filter) }

      if !callback && options[:raise]
        raise ArgumentError, "#{type.to_s.capitalize} #{name} callback #{filter.inspect} has not been defined"
      end

      if callback && (options.key?(:if) || options.key?(:unless))
        new_callback = callback.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless])
        chain.insert(chain.index(callback), new_callback)
      end

      chain.delete(callback)
    end
    target.set_callbacks name, chain
  end
end

def define_callbacks(*names)
  options = names.extract_options!

  names.each do |name|
    name = name.to_sym

    set_callbacks name, CallbackChain.new(name, options)

    module_eval <<-RUBY, __FILE__, __LINE__ + 1
      # do great work
    RUBY
  end
end

```
各コールバックメソッドは一つの`CallBack`配列を利用して、管理しています。
`set_callback`するとき、各コールバックメソッドを`CallBack`配列に追加します。
`skip_callback`するとき、skipするコールバックメソッドを`CallBack`配列から除きます。

- **メリット**

一つの配列を利用するので、管理しやすいです。

- **デメリット**

`skip_callback`を呼ぶタイミングに気をつける必要があります。
なぜなら、`set_callback`を呼ぶ前に、`skip_callback`を呼んでしまうと、skipしたいコールバックメソッドは`CallBack`配列内にまだ追加されていないため、skipできないです。

## 3.2. 上で取り上げた問題のケース
おそらく、`skip_callback`を呼ぶとき、`CallBack`に`push_notification`が追加されてないので、動かなかった。
`set_callback`と`skip_callback`が呼ばれる順番を確認しましょう。

```rb:models/article.rb
class Article
  has_one :draft_article, dependent: :destroy, class_name: TemporaryArticle
  after_save :push_notification
end
```
```rb:models/temporary_article.rb
class TemporaryArticle < Article
  belongs_to :public_article, class_name: Article.name
  skip_callback :save, :after, :push_notification
end
```
[autoload_paths](http://guides.rubyonrails.org/autoloading_and_reloading_constants.html)により、ファイルロードの流れは
**1. `models/temporary_article.rb`を読み込む**
**2. `temporary_article.rb`の行１を読む**
　　- `TemporaryArticle`定数を見つけるが、この前、`class`キーワードがありますので、定数ではないと判断されます。`autoloading`がトリガーされなかった。
　　- 次、`Article`定数を見つけます。これは定数なので、`autoloading`がトリガーされます=>`models/article.rb`をロードします。
**3. `models/article.rb`の行２を読む**
　　- `TemporaryArticle`定数を見つけます、これは定数なので、`autoloading`がトリガーされます　=>`models/temporary_article.rb`をロードします。
**4. `temporary_article.rb`は引き続き読む**
　　-行2`Article`定数を見つけるが、前に見つけましたので、`autoloading`がトリガーされない
　　-行３ `skip_callback`を呼ぶ
**5. `article.rb`は引き続き読む**
　　- `after_save`コールバックを呼ぶ

**結局、`skip_callback`を呼んでから、`after_save`コールバックを呼ばれます。
`skip_callback`が効かなかった。**

### 解決策
案１：　implicit(暗黙定義)
`class_name: TemporaryArticle`を使わずに直接参照します。

```rb
例
 #has_one :draft_article, dependent: :destroy, class_name: TemporaryArticle
 has_one :temporary_article, dependent: :destroy
```
案２：　できれば、各コールバックは上に移動する

```rb:models/article.rb
class Article
  #move this line to the bellow of callback methods
  #has_one :draft_article, dependent: :destroy, class_name: TemporaryArticle

  after_save :push_notification

  # move to here
  has_one :draft_article, dependent: :destroy, class_name: TemporaryArticle
  
end
```

案３：`CallBack`のアルゴリズムを変える
今現在、一つの`CallBack`配列を利用して、管理していますが、デメリットがあります。
もし、`callback`と`skip_callback`メソッドを分けて、別々配列で管理すれば、問題解決できるかなと思います。

## 4.参考
http://guides.rubyonrails.org/autoloading_and_reloading_constants.html