ネストしたモジュールを書く場合の注意点

rubyflow経由で読んだ「How You Nest Modules Matters in Ruby」の内容を紹介する。

ネストしたモジュールを記述する際、シンタックスの違いで挙動が異なることについての記事だ。


Rubyではネストしたモジュール(とクラス)を記述するために2つの異なるシンタックスが用いられる。

# シンタックス#1
module API
  module V1
  end
end
 
# シンタックス#2
module API::V1
end

シンタックス#2は既に存在するAPIモジュールを必要とする。

この2つのシンタックスは等価で、どちらを使用するかは好みの問題だと考えている人もいるが、これは誤りだ。

バージョン管理されたREST APIの例を用いてこれを説明する。

module API
  class Responder
  end
 
  module V1
    class Controller
      def action
        Responder.respond_with('Hello, World!')
      end
    end
  end
end

#actionが呼び出された時、Rubyは以下の順番でResponderが定義されているかルックアップする。

  1. API::V1::Controller
  2. API::V1
  3. API
  4. トップレベ

上記のコードの場合、3.でAPI::Responderとしてルックアップされる。

Module.nestingメソッドでモジュール/クラスのネスト状態を調べることができる。これを用いてシンタックス#1と#2の違いを捉える。

# シンタックス#1
module API
  class Responder
  end
 
  module V1
    class Controller
      p Module.nesting #=> [API::V1::Controller, API::V1, API]
 
      def action
        Responder.respond_with('Hello, World!')
      end
    end
  end
end
# シンタックス#2
module API
  class Responder
  end
end
 
module API::V1
  class Controller
    p Module.nesting #=> [API::V1::Controller, API::V1]
 
    def action
      Responder.respond_with('Hello, World!')
    end
  end
end

シンタックス#2の場合、#actionが読み出されるときNameErrorが発生する。

NameError: uninitialized constant API::V1::Controller::Responder

シンタックス#2の場合、ネスト状態の情報が失われてしまうことが原因だ。 API内にResponderが定義されているかルックアップを行わないため、例外が発生してしまう。

結論

ネストしたモジュールを記述する場合、シンタックスによる違いを意識しよう。