Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyでモジュールをネストするときに避けるべき落とし穴

Rubyでは、モジュール(やクラス)をネストして定義するのが一般的です。ActiveRecord::RecordNotFound のようなコードはあまりにもよく目にするため、深く考えることもほとんどないでしょう。しかし、Rubyのネストの実装やRailsのオートロードシステムには、コードを予期せぬ形で壊してしまう罠がいくつか潜んでいます。この記事では、それらの罠の原因と回避方法を解説します。

定数とは何か?

この記事のテーマはモジュールですが、その理解にはまず「定数」について知る必要があります。多くのプログラミング言語では、定数は次のように小さなデータを格納するために使われるだけです。

# シンプルな定数
MAX_RETRIES = 5
DEFAULT_LANGUAGE = "en"

ところがRubyでは、クラスやモジュールもまた定数なのです。以下の例を見てください。このモジュールには3つの定数が含まれています。数値、クラス、そしてネストされたモジュールです。ネストされたクラスやモジュールへアクセスするとき、Rubyは単純な数値定数とまったく同じルールでそれを探し出します。

module MyModule
  MY_FAVORITE_NUMBER = 7

  # クラスも定数の一種
  class MyClass
  end

  # モジュールも同様
  module MyEmbeddedModule
  end
end

puts MyModule.constants.inspect # => [:MY_FAVORITE_NUMBER, :MyClass, :MyEmbeddedModule]

多くの場合、モジュールは親で定義された定数にアクセスできます。だからこそ、次のようなコードが書けるのです。

module X
  MARCO = "polo"
  module Y
    def self.n 
      puts MARCO
    end
  end
end

X::Y.n() # => "polo"

しかし、「YからXの定数MARCOにアクセスできるのは、親子関係があるからだ」と考えているなら、それは誤りです。

よくある問題

先ほどのコードを少し別の書き方に変えると、驚くべきことが起こります。Yはもう X::MARCO にアクセスできなくなるのです。一体何が起きているのでしょうか?

module A
  MARCO = "polo"
end

module A::B
  def self.n 
    puts MARCO  # => uninitialized constant A::B::MARCO (NameError)
  end
end

A::B.n()

実は、親の定数が子に「継承」されるように見える現象は、親子関係によるものではありません。それは字義的(レキシカル)なものです。つまり、コードが生成するオブジェクトの構造ではなく、ソースコード自体の構造に基づいて定数が解決されているのです。

Module.nestingでネストを調べる

Rubyがネストされた定数をどのように検索しているのか、より深く理解したいなら、Module.nesting メソッドを調べてみる価値があります。

このメソッドは、あるスコープにおける定数の「探索パス」を構成するオブジェクトの配列を返します。先ほどの例のネストを確認してみましょう。

最初の例では、ネストは [A::B, A] になっています。これは、定数MARCOを参照したとき、Rubyがまず A::B 内を探し、見つからなければ次に A 内を探すという意味です。

module A
  MARCO = "polo"
  module B
    def self.n 
      puts Module.nesting.inspect  # => [A::B, A]
      puts MARCO # => "polo"
    end
  end
end

一方、2番目の例では、ネストには A::B だけが含まれ、A は含まれていません。Bは A の「子」ではありますが、この書き方では両者がネスト関係にあるようには見えないため、定数の探索という観点では無関係も同然なのです。

module A
  MARCO = "polo"
end

module A::B
  def self.n 
    puts Module.nesting.inspect  # => [A::B]
    puts MARCO # => uninitialized constant A::B::MARCO (NameError)
  end
end

Railsのオートロードによる複雑化

Railsを使っているとき、ファイルをrequireしなくてもよいことに気づいたことはありませんか? モデルを使いたければ、宣言なしにそのまま使うだけで動きます。

これが可能なのは、Railsがオートロードシステムを実装しているからです。Railsは Module.const_missing をフックして、まだ読み込まれていない定数への参照を検出し、その定数を含んでいるはずのファイルを自動的に読み込みます。これはたいていの場合うまく機能しますが、注意点があります。

Railsは、モジュールが常に最大限のネストを持っていると想定しています。つまり、モジュール A::B::C のネストは必ず [A::B::C, A::B, A] であると決めつけているのです。実際のネストがそれと異なる場合、予期しない動作が発生します。

以下のコードでは、モジュールBは本来 A::MARCO にアクセスできないはずです。通常のRubyでは、Bのネストが [A::B] しかないため例外が発生します。ところが、Railsのオートロード環境下では例外は投げられず、代わりに A::MARCO が返されてしまうのです。

# a.rb
module A
  MARCO = "polo"
end

# a/b.rb
module A::B
  def self.n 
    puts MARCO # => "polo"
  end
end

# some_controller.rb
A::B.n()

まとめ

ここまで見てきたように、モジュールのネストには考慮すべき点が多くあります。筆者としては、できる限り複雑なことを考えずに済むスタイルを選びたいので、module A::B という構文は極力避けるようにしています。モジュールのネストを意図的に操作したいケースは、正直なところ思い浮かびません。もし実用的な活用例をご存じでしたら、ぜひ教えてください!

  1. Googleで検索しないほうがいい12のこと|知らずに危険にさらす検索ワードとは

    何かを調べたいとき、私たちがまず頼りにするのがGoogleです。近くの銀行支店の場所から、大手企業の最新ニュースまで、あらゆる疑問に対して答え、あるいは答えへと至る最適な道筋を示してくれる万能ツールとなっています。しかし、便利さの裏にはリスクも潜んでいます。オンライン上のあらゆる行動はどこかに痕跡を残します。家族にも話せないような機密性の高い情報が追跡され、悪意ある第三者に利用される可能性もあるのです。ハッキングや情報漏洩のニュースが後を絶たない今、何を検索するかという小さな選択が、大きな被害につながることもあります。ここでは、Googleで検索すると危険を招きかねない12の項目を詳しく解説し

  2. 知っておきたい!Windowsメンテナンスで避けるべき5つの間違い

    テクノロジーの進化に伴い、多くのユーザーはWindowsパソコンの基本的なメンテナンス方法を身につけ、定期的にクリーンアップしてPCを健康な状態に保つようになりました。しかし、OS自体も時代とともに大きく変化しており、特にWindows 10ではメンテナンスのあり方も一変しています。ところが、その変化がユーザーに十分伝わっていないため、今でも古い方法でメンテナンスを行い、かえってシステムに悪影響を及ぼしているケースが少なくありません。ご安心ください。この記事では、PCの健康を維持する際に避けるべき、よくある間違いを5つ紹介します。1. Windows Updateのインストールを先延ばし・無視