ITエンジニアによるITエンジニアのためのブログ

Rubyのネストクラス

Rubyのコードでは、クラス定義の中に別のクラス定義がネストされて書かれていることがあります(便宜上、この文章ではネストクラスもしくは内側クラスと呼ぶことにします)。例えば、以下のようなコードが該当します。

class Outside
  def outside_method
    puts '外側のメソッド'
  end

  class Inside
    def inside_method
      puts '内側のメソッド'
    end
  end
end

ネストクラスは、以下のような形で新しいインスタンスを作成することができます。

inside = Outside::Inside.new
inside.inside_method

このコードが正常に実行されることからわかるように、InsideクラスのインスタンスはOutsideクラスの外側でも生成可能です。つまり、プライベートメソッドのようにOutsideクラス内だけでしか使えないと言った言語上の制約はRubyには存在しないということです。

実際、上記のInsideのようにネストされたクラスはOutsideクラスについての情報を一切持っていません。このことは、Insideクラスのクラス継承情報を見てみるとわかります。

Outside::Inside.ancestors
-> [Outside::Inside, Object, Kernel, BasicObject]

つまるところ、InsideクラスをOutsideクラスの外側に作った場合とのRuby言語上の違いはクラスの名前空間だけです。

class Outside
  def outside_method
    puts '外側のメソッド'
  end
end

class Inside
  def inside_method
    puts '内側のメソッド'
  end
end

inside = Inside.new
inside.inside_method

ではなぜ、わざわざクラスの中で別のクラスを定義するのかと言うと、その方がクラスの使い道についての意図を他のエンジニアに明確に伝えることが出来るからです。

ネストクラスを使う時は、大きく分けて以下のいずれかの意図を伝えたい時です。

  1. 内側クラスは外側クラス内のコードでしか使われないことを示唆する。
  2. 内側クラスは外側クラス以外の場所でも単体で使用するが、2つのクラスの関連性を明確にする。

内側クラスは外側クラス内のコードでしか使われないことを示唆する

これは、プライベートメソッドのクラスバージョンと言えばイメージしやすいかもしれません。例えば、CSVを生成してメール添付するバックグラウンドジョブのクラスを考えてみましょう。

このCSVがこのバックグラウンドジョブ以外で使われない場合、CSVクラスを汎用的にトップレベルの名前空間を使って定義してしまうと、CSVが他の場所でも使われるとの印象を他のエンジニアに与えてしまう可能性があります。

このような場合は、CSVクラスをバックグラウンドジョブの中にネストすることでそのような誤解を防ぐことができます。

以下は、Ruby on Railsのバックグラウンドジョブでの例です。

# app/models/send_csv_job.rb

class SendCsvJob < ApplicationJob
  def perform(data)
    @csv_attachment = CsvAttachment.new(data)
  end

  class CsvAttachment
  end
end

なお、このようなケースでは、上記のように1つのファイルに両クラスのコードをまとめて書く方が意図が伝わりやすいです。

内側クラスは外側クラス以外の場所でも単体で使用するが、2つのクラスの関連性を明確にする

例えば、オフィスについてのクラスと、そこにある椅子についてのクラスを作りたい時、選択肢としては以下の2つが考えられます。

1つ目の方法は、オフィスクラスと椅子クラスをそれぞれ独立して定義する方法です。Ruby on Railsのモデルだとすると以下のようになります。

# app/models/office.rb
class Office < ApplicationRecord
end

# app/models/chair.rb
class Chair < ApplicationRecord
end

これでもコードを実行するのに問題はありませんが、椅子クラスについてはオフィス用の椅子なのか、家用の椅子なのかがはっきりしません。汎用的な椅子クラスを定義するなら上の方法が適していますが、オフィス用の椅子を定義する方法としては不適切です。

もう一つは、オフィスクラスの名前空間の中で椅子クラスを定義する方法です。

# app/models/office.rb
class Office < ApplicationRecord
end

# app/models/office/chair.rb
class Office::Chair < ApplicationRecord
end

この方が、オフィス内にある椅子、もしくはオフィス用の椅子という意図がはっきり伝わります。将来的に家用の椅子が必要になった際でも、他のエンジニアがオフィス用の椅子クラスを使って実装してしまうリスクはほとんどないでしょう。

このようなケースではネストクラスは外側のクラスとは別に個別ファイルに記載した方が、ネストクラスは外側クラス内だけでなくアプリケーション全体で使えるという意図が明確になります。

アプリケーション全体で使えるということの意味は、例えばオフィス用椅子のコントローラーでOffice::Chairクラスのインスタンスを生成するといった場合を指します。

app/controllers/office/chairs_controller.rb

class Office::ChairsController < ApplicationController
  def new
    @office_chair = Office::Chair.new
  end
end

おまけ:モジュールかクラスか

外側をクラスとするかモジュールとするかは、内側のクラスに関しては関係ありません。以下のように外側をモジュールとしても、内側クラスの名前空間はクラスの場合と一緒です。

module Outside
  def outside_method
    puts '外側のメソッド'
  end

  class Inside
    def inside_method
      puts '内側のメソッド'
    end
  end
end

inside = Outside::Inside.new
inside.inside_method

外側をモジュールとするかクラスとするかは、外側でインスタンスを作成する必要があるかどうかの問題であり、内側クラスにとっては関係ない話です。

おまけ2:ネストクラス記載方法

外側のクラスやモジュールが既に別の場所で定義されており、内側のクラス用に名前空間だけを必要としている場合は、以下のように簡潔に定義することも可能です。

# 別のファイルで定義済み
class Outside
  def outside_method
    puts '外側クラスのメソッド'
  end
end

# 内側クラスのファイルには以下のみを記載
class Outside::Inside
  def inside_method
    puts '内側クラスのメソッド'
  end
end

ただし、Outsideクラスやモジュールが既に定義されていない場合は、uninitialized constant Office (NameError)というエラーになってしまいますので、気をつけて下さい。

まとめ

rubyではネストクラスは名前空間だけのコンセプトで、言語レベルでのインターフェースの制約は存在しません。一方、ネストクラスを使うとクラスの使い方に関して明確な意図を表現することが可能になります。

自身が書いたコードの意図を明確にすると、他のエンジニアとのコミュニケーションも円滑に進みますので、ぜひ積極的に使ってみてください。