Rails で Class がうまく読み込まれない問題と戦った話

## この記事は？

Rails 3.2 で Class (Module) の読み込みで問題が発生して困ったので、
その問題を解決するまでの顛末についてまとめた備忘録です。

具体的には
`warning: toplevel constant Y referenced by X::Y`
というエラーに対処した話です。

※ Rails 4 では検証していません。

## 発生した問題について

とある Model のデータの平均値を計算して、その結果をクライアンサイドでチャート表示したい。
それを実現するために `app/models` 配下に、`ActiveRecord::Base` を **継承しない** `Chart` というオリジナルの Class を設けました。
その他に平均値を計算したり、チャートを描画するための専用フォーマットのデータを作るための Class が欲しかったので、
そのための設計を行いました。

最終的なファイル構成は以下の通りです。

```
app/
 └ models/
     ├ chart/
     │  ├ average/
     │  │  └ set.rb
     │  │
     │  ├ data/
     │  │   ├ hoge.rb
     │  │   └ fuga.rb
     │  │
     │  ├ average.rb
     │  └ data.rb
     │
     ├ chart.rb
     │
     ├ model_a.rb
     ├ model_b.rb
     〜
     └ model_n.rb
```

``` chart.rb
class Chart
  def initialize(arg1, arg2)
    @arg1 = arg1
    @arg2 = arg2
  end
end
```

``` chart/average.rb
class Chart
  class Average
  end
end
```

``` chart/average/set.rb
class Chart
  # 平均値の集合を表す Class
  class Average::Set
  end
end
```

``` chart/data.rb
class Chart
  # Mix-in 用の Module
  module Data
  end
end
```

``` chart/data/hoge.rb
class Chart
  class Data::Hoge
    include Data
  end
end
```

``` chart/data/fuga.rb
class Chart
  class Data::Fuga
    include Data
  end
end
```

この状態で rails console を起動し、各 Class (Module) を確認すると、
以下のようにいくつかの Class (Module) がうまくロードされていないことが分かりました。

``` ruby
Loading development environment (Rails 3.2.18)
[1] pry(main)> Chart
=> Chart
[2] pry(main)> Chart::Data
(pry):2: warning: toplevel constant Data referenced by Chart::Data
=> Data
[3] pry(main)> Chart::Data::Hoge
(pry):3: warning: toplevel constant Data referenced by Chart::Data
NameError: uninitialized constant Data::Hoge
from (pry):3:in `<main>'
[4] pry(main)> Chart::Data::Fuga
(pry):4: warning: toplevel constant Data referenced by Chart::Data
NameError: uninitialized constant Data::Fuga
from (pry):4:in `<main>'
[5] pry(main)> Chart::Average
=> Chart::Average
[6] pry(main)> Chart::Average::Set
(pry):6: warning: toplevel constant Set referenced by Chart::Average::Set
=> Set
```

`warning: toplevel constant Data referenced by Chart::Data`
**なだこれは！！！** (CV: 宮野真守)

どうやら `Chart::Data` や `Chart::Average::Set` などは既存の Class ([Data](http://docs.ruby-lang.org/ja/2.1.0/class/Data.html) と [Set](http://docs.ruby-lang.org/ja/2.1.0/class/Set.html)) の名前と衝突しており、
その結果うまくロードされていないようです。
一方、`Chart::Average` は名前が衝突していないためか、正しく読み込まれています。

``` ruby
Loading development environment (Rails 3.2.18)
[1] pry(main)> Chart::Data.equal?(::Data)
(pry):2: warning: toplevel constant Data referenced by Chart::Data
=> true
[2] pry(main)> Chart::Average::Set.equal?(::Set)
(pry):3: warning: toplevel constant Set referenced by Chart::Average::Set
=> true
```

※ [BasicObject#equal?](http://docs.ruby-lang.org/ja/2.1.0/class/BasicObject.html#I_EQUAL--3F) は2つのオブジェクトが同一のものかどうかを判定するためのメソッドです。

だからと言って、衝突を避けるために Data や Set 以外の名前を使ってしまったら負けかなと思ったので、
この問題を解決すべく奮闘しました。

## 解決方法: アプリケーション初期化時に require する

[Preventing “warning: toplevel constant B referenced by A::B” with namespaced classes in Rails](http://stackoverflow.com/questions/9807827/preventing-warning-toplevel-constant-b-referenced-by-ab-with-namespaced-cla)
という Stack Overflow の記事を参考に、
`config/initializers` ディレクトリを利用して、
アプリケーション初期化時に `models/chart` ディレクトリ以下のファイルが読み込まれるようにしてみました。

``` config/initializers/requirements.rb
# 上位のディレクトリにあるファイルを優先して require する。
require Rails.root.join("app/models/chart/data")
require Rails.root.join("app/models/chart/data/hoge")
require Rails.root.join("app/models/chart/data/fuga")
require Rails.root.join("app/models/chart/average")
require Rails.root.join("app/models/chart/average/set")

# 代わりに以下のコードでもよい。
# Dir[Rails.root.join("app/models/chart/**/*.rb")].sort.each { |f| require f }
```

このファイルを追加して再度 rails c で require した Class が
正しく読み込まれているか試してみます。

``` ruby
Loading development environment (Rails 3.2.18)
[1] pry(main)> Chart
=> Chart
[2] pry(main)> Chart::Data
=> Chart::Data
[3] pry(main)> Chart::Data::Hoge
=> Chart::Data::Hoge
[4] pry(main)> Chart::Data::Fuga
=> Chart::Data::Fuga
[5] pry(main)> Chart::Average
=> Chart::Average
[6] pry(main)> Chart::Average::Set
=> Chart::Average::Set
[7] pry(main)> Chart::Data.equal?(::Data)
=> false
[8] pry(main)> Chart::Average::Set.equal?(::Set)
=> false
```

おお、うまく読み込まれたみたいですね！
✌ ('ω' ✌ )三 ✌ ('ω') ✌ 三( ✌ 'ω') ✌

ただし、development 環境でサーバを起動 (rails s) してチャートを描画している画面にアクセスするとなぜか
`ArgumentError: wrong number of arguments (2 for 0)` というエラーが。
上に示したコード例の通り `Chart` のコンストラクタは2個の引数を受け取るように実装しているのだが、なぜか引数の数が0個になってしまっているのです。
`Chart` クラス自体はちゃんと読み込まれているのに…謎。

もちろん、コードを `Chart` クラスを追加する前の状態に戻すと `Chart` クラスは存在していません。
ですので、既存の Class と衝突している可能性はないはずです。

``` ruby
Loading development environment (Rails 3.2.18)
[1] pry(main)> Chart
NameError: uninitialized constant Chart
```

試しに `config/initializers` 以下のファイルで `app/models/chart.rb` も require するようにしてみると、なぜか解決しました。

``` config/initializers/requirements.rb
require Rails.root.join("app/models/chart") # この行を追加した。
require Rails.root.join("app/models/chart/data")
require Rails.root.join("app/models/chart/data/hoge")
require Rails.root.join("app/models/chart/data/fuga")
require Rails.root.join("app/models/chart/average")
require Rails.root.join("app/models/chart/average/set")
```

最終的には `config/initializers` 以下では `app/models/chart.rb` のみを require するようにし、
`app/models/chart.rb` でサブディレクトリ以下のファイルを require するようにしました。

``` config/initializers/requirements.rb
require Rails.root.join("app/models/chart")
```

``` app/models/chart.rb
Dir[Rails.root.join("app/models/chart/**/*.rb")].sort.each { |f| require f }

class Chart
  def initialize(arg1, arg2)
    @arg1 = arg1
    @arg2 = arg2
  end
end
```

以上で今回作成した Class (Module) が全て正しく読み込まれるようになりました。

## その他気になった点

問題に遭遇した当初は、
「development 環境で、サーバを起動して初回アクセス時はチャートの描画処理が正しく動作するのに、
2回目以降のアクセス時には
`NameError: uninitialized constant Chart::Data` が発生してしまう。」
という謎の問題にも頭を悩まされました。
しかし、記事を書くにあたってその問題を再現させようと試しましたが再現できませんでした…。これまた謎。
この問題は [Rails.application.config.cache_classes](http://railsdoc.com/config#%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E3%83%AA%E3%83%AD%E3%83%BC%E3%83%89%E3%82%92%E3%81%99%E3%82%8B%E3%81%8B%E3%81%AE%E8%A8%AD%E5%AE%9A(config.cache_classes)) が関係してるんじゃないかと思ってますが
再現しないために検証できなかったです。ごめんなさい…orz

## 参考

* [[Qiita] クラスロード問題ではまる]([Stack Overflow] http://qiita.com/tanaka51/items/c8873319689217bb81a9)
* [[Stack Overflow] Preventing “warning: toplevel constant B referenced by A::B” with namespaced classes in Rails](http://stackoverflow.com/questions/9807827/preventing-warning-toplevel-constant-b-referenced-by-ab-with-namespaced-cla)
* 
[[Stack Overflow] Rails: Elegant way to structure models into subfolders without creating submodules]
(http://stackoverflow.com/questions/1445341/rails-elegant-way-to-structure-models-into-subfolders-without-creating-submodul)
* [[GitHub] YY::XX always references XX if XX was defined #6931](https://github.com/rails/rails/issues/6931)

## おまけ

最近 [A Way to Organize POROs in Rails](http://vrybas.github.io/blog/2014/08/15/a-way-to-organize-poros-in-rails/) という記事に感化されています。