Sansan Builders Box

Sansanのものづくりを支える技術やデザイン、プロダクトマネジメントの情報を発信

「クラスはオブジェクトである」に辿り着くまで

始めに

初めまして、DSOC エンジニアの冨田です。

突然ですが、明後日は何の日でしょうか?
そう、明後日は待ちに待った RubyKaigi です!
楽しみですね、実は今まで RubyKaigi に参加したことがなく初参加になるので、個人的には Rubyist として一歩前に進めたような気がしてます。たとえ登壇内容が高度過ぎて理解できなくても、その場に行き刺激を受けてきたいと思います。

Sansanに転職後、 Ruby や Rails を使って開発をすることになったため、日々学びと気づきの連続です。そんな日々を改めて振り返ると Ruby に対する理解が進んだなと感じる瞬間があったように思います。
それは「クラスはオブジェクトである」ことを理解したときです。

ということで、本記事では「クラスはオブジェクトである」ことの内実について解説していきます。世に解説記事は出回っており、すでに理解している人も多いかとは思いますが、改めて整理していきたいと思います。

前提知識

早速本題に行きたいところなのですが、その前に理解しておくべき前提知識について整理します。
それは、そもそも「オブジェクト、クラスとは何なのか」ということです。それに答えるために、オブジェクトの階層構造について説明します。ここで「あるオブジェクトを作った時にその裏側でどのようなことが起きているのか」を把握します。前提知識と書きましたが、この内容が本記事のメインになります。思ったよりも長くなってしまったので、理解している箇所は読み流してもらえればと思います。

説明用として、以下のコードを用意しました。

class Parent
  def p_method
    "#{self} call p_method"
  end
end

class Child < Parent
  def initialize(name)
    @name = name
  end

  def c_method
    "#{self} call c_method"
  end
end

chd = Child.new('taro')


この時、オブジェクトの階層構造はどうなっているのでしょうか?
図で示すと下のようになります。[*1]

f:id:tommy_0:20190410074508p:plain

それでは次に、階層構造を構成する各要素について解説していきます。
他にも説明の仕方はありますが、機能的に整理すると以下のようになるかと思います。

オブジェクト

図のどれが該当するか

全てが該当します。「なぜそう言えるのか」を説明するのが本題になります。この内容については後ほど説明します。

どんな機能を持つか
  • インスタンス変数の保持

オブジェクトごとに専用のインスタンス変数の一覧を持つことで、オブジェクトごとに状態を管理できます。また、インスタンス変数はインスタンスメソッドの中で定義されるため、同じクラスのオブジェクトであってもインスタンスメソッド呼び出し有無によってインスタンス変数が異なることがあります。

  • メソッドの呼び出し

オブジェクトに対して唯一可能な操作であり、クラスで定義されたメソッドを呼び出すことができます。
メソッドを呼び出した時には、まずメソッドの探索が行われます。メソッド自体はオブジェクトに存在しないため、メソッドを呼び出したオブジェクト(レシーバ)の特異クラスから順番に、該当のメソッドを見つけるまで継承階層を登っていきます。そして、メソッドを実行する時には、そのレシーバをカレントオブジェクトとして処理が行われます。例えば、図にある赤線で示されているのは、chd をレシーバとして p_method を呼び出した時の流れになります。

カレントオブジェクト

インタプリタが追跡している実行環境となるオブジェクトを指します。Rubyのコードは常にカレントオブジェクトの内部で実行されます。
カレントオブジェクト は self で取得することができ、メソッド内で取得した場合はそのレシーバ、メソッド外(クラス/モジュール内)で取得した場合はそのクラス/モジュールを返します。先程「インスタンス変数はインスタンスメソッドの中で定義される」と書きましたが、それは言い換えれば「レシーバ内でインスタンス変数を定義する」ということになります。

クラス

図のどれが該当するか

大文字から始まるものが該当します。例えば、Child などです。

どんな機能を持つか
  • インスタンス化

クラスからオブジェクトを生成できます。全てのオブジェクトは何かしらのクラスから生成されます。

  • インスタンスメソッドの定義

オブジェクトから呼び出せるインスタンスメソッドを定義できます。
クラスはオブジェクトの振る舞いを規定する役割があるため、メソッド自体はオブジェクトではなくクラスに存在します。

  • 親クラスの継承

クラスには継承元となる親クラスが存在します。メソッド探索範囲に親クラスも含まれるため、オブジェクトから親クラスのインスタンスメソッドを呼び出すことができます。

カレントクラス

カレントオブジェクトと同様に、インタプリタが追跡している情報です。メソッド定義を行うと、それはカレントクラスのインスタンスメソッドになります。
カレントオブジェクトは self で参照できますが、カレントクラスを参照するキーワードはありません。カレントクラスは、メソッド内ではカレントオブジェクトのクラス、メソッド外(クラス/モジュール内)ではそのクラス/モジュールになります。

特異クラス

図のどれが該当するか

#から始まるものが該当します。例えば、#Child などです。
本題を説明する際に重要な概念となるので、いくつか補足したいと思います。
特異クラスとは、特定のオブジェクトのみに適用されるクラスのことであり、オブジェクトを一つしか持てないため別名シングルトンクラスと呼ばれます。
普段はその存在を意識することはないかもしれませんが、以下のようにその実態を確認できます。

chd.singleton_class
=> #<Class:#<Child:0x00007fb0bba21138>>

class Child
  class << self
    self
  end
end
=> #<Class:Child>
どんな機能を持つか
  • 特異メソッドの定義

特定のオブジェクトからのみ呼び出せる特異メソッドを定義できます。

  • 親クラスの継承

特異クラスには継承元となる親クラスが存在します。特異クラスの適用対象によって親クラスは異なります。オブジェクトの特異クラスの場合はそのオブジェクトのクラス、クラスの特異クラスの場合はそのクラスの親クラスの特異クラスになります。メソッド探索範囲に特異クラスも含まれるため、オブジェクトから特異メソッドを呼び出すことができます。

f:id:tommy_0:20190410075013p:plain

モジュール

図のどれが該当するか

Kernel のみが該当します。
モジュール単体では継承階層の中には存在しないのですが、モジュールが読みこまれる(include 等が実行される)と、インタプリタが指定されたモジュールの無名クラスを作成し継承階層の中に組み込みます。
この無名クラスの存在は superclass では確認できませんが、ancestors で確認できます。

Child.ancestors
=> [Child, Parent, Object, Kernel, BasicObject]
どんな機能を持つか
  • メソッドの取り込み

クラスや他モジュールにメソッドを取り込むことができます。取り込み形式は2種類あり、取り込み元がクラスの場合にはインスタンスメソッド、特異クラスの場合には特異メソッドとして取り込まれます。

  • 名前空間の提供

モジュール定義の中には別モジュールやクラスを定義できます。大文字で始まる参照(クラス名やモジュール名)は定数であり、これら定数はスコープを持つため、ネストすることでスコープ階層を作ることができます。
クラス定義でも同様のことが行えますが、クラスはそれに加えてインスタンス化や継承を行えるので、目的に応じて使い分けその意図を明確にする観点から通常はモジュールを使います。

  • 関数の定義

モジュール単体から呼び出せる関数を定義できます。実態としてはモジュールの特異メソッドになります。


この辺りの内容は、メタプログラミングRubyパーフェクトRuby などの書籍で説明されているので一読することをお勧めします。


本題

ようやく本題です。上記の内容を踏まえて「クラスはオブジェクトである」ことを確認してみましょう。

具体的には、オブジェクトに備わっている性質や機能がクラスにも当てはまることを確認してみます。

  • インスタンスであること
  • インスタンス変数を持てること
  • メソッドの呼び出しが行えること

インスタンスであること

全てのオブジェクトのクラスが Class クラスになることから予想がつくかと思いますが、class 定義式の実態は「定数(クラス名)に、Class クラスのオブジェクトを割り当てる」ということなので、クラスは Class クラスから生成するができます。

Child = Class.new(Parent) do
  def initialize(name)
    @name = name
  end

  def c_method
    "#{self} call c_method"
  end
end

インスタンス変数を持てること

カレントオブジェクトの説明にあったようにクラス内での self はクラスそのものなので、クラス内でインスタンス変数を宣言すれば、クラスにインスタンス変数を持たせることができます。

class Child
  @birth_year = 2019
end

Child.instance_variables
=> [:@birth_year]

メソッドの呼び出しが行えること

メソッド呼び出しの説明にあったように、メソッド呼び出しを行うと、「該当のメソッドを見つけるためにレシーバ(メソッドを呼び出したオブジェクト)の特異クラスから順番に継承階層を登っていき、メソッドを実行する時にはレシーバをカレントオブジェクトとして実行する」という流れでした。
クラスの特異メソッドである「クラスメソッド」を定義することで、クラスでもオブジェクト同様にメソッド呼び出しが行えます。

class Parent
  class << self
    def p_singleton_method
      "#{self} call p_singleton_method"
    end
  end
end

Child.p_singleton_method
=> "Child call p_singleton_method"

#Parent に特異メソッドが定義されるため、Child から #Child を経由して該当メソッドを呼び出すことができます。
f:id:tommy_0:20190410075739p:plain

また、上記のようにクラスメソッドを別途定義しなくても、クラスからメソッド呼び出しを行うことができます。 全てのクラスの特異クラスの継承階層に Object クラスが存在するので、Object クラスのメソッド(Kernel モジュールからインクルードしたメソッド群)、例えば class メソッドなどを呼び出すことができます。つまり、Object クラスや BasicObject クラスのメソッドはインスタンスメソッドとしてもクラスメソッドとしても利用できることになります。

おまけ

ここまで、オブジェクトの階層構造の整理から始まり「クラスはオブジェクトである」ことを確認してきました。
個人的に一番学びが大きかったのは、「特異クラスの存在によって、クラスから親クラスのクラスメソッド呼び出せる」ということでした。
では、ここまでの理解を確認するため、特異クラスを利用する場面として多いであろう「モジュールをインクルードしてクラスメソッドを取り込む方法」を改めて確認してみたいと思います。

最初に正常に機能しない実装を載せます。

module ChildRights
  class << self
    def c_singleton_method
      "#{self} call c_singleton_method"
    end
  end
end

class Child
  include ChildRights
end

Child.c_singleton_method
=> NoMethodError (undefined method `c_singleton_method' for Child:Class)

上記だと、モジュールのインスタンスメソッドは利用できますがクラスメソッドは利用できません。なぜなら、特異クラス(#Child)ではなく通常のクラス(Child)が「モジュールから生成された無名クラス」を継承しているからです。

特異クラスに継承させるよう、正しくは下記のようにする必要があります。
特異クラスをオープンし、その中でインクルードを行います。

module ChildRights
  def c_singleton_method
    "#{self} call c_singleton_method"
  end
end

class Child
  # 下記 3 行は extend ChildRights と書くこともできる
  class << self
    include ChildRights
  end
end

Child.c_singleton_method
=> "Child call c_singleton_method"


ちなみに、Rails では ActiveSupport::Concern という仕組みを導入し、Ruby のデフォルトの挙動を変更することで、モジュールのインクルードによってインスタンスメソッドとクラスメソッドの両方を取り込むことができます。また、利用する分には意識する必要がないのですが、内部で行なっている面白い点として、モジュール同士の依存(インクルード対象のモジュールが別のモジュールをインクルードしている)に伴う問題(クラスメソッドの取り込みが適切に行われない)を、モジュール同士の依存関係をトラッキングすることで解決しています。気になる人は調べてみると新しい発見があるかもしれません。

また、ここまでの内容を理解できているかの確認として検定資格を受けてみるのもいいかと思います。

終わりに

以上で解説は終わりです。
記事を書いての所感、これからの抱負に触れて締めたいと思います。

今回の内容を含め、Ruby について学ぶほどに Ruby そのものについてもまだまだ知るべきことがたくさんあると感じる一方、それと並行してこれら知識を使って何ができるのか考えることも重要だなと思う今日この頃です。身近なところで言えば「どのような場面でクラス拡張を行うのが適切なのか」など、もう少し離れたところで言えば、gem を作ったり OSS に貢献したりと様々あります。一言で言えば、学んだ知識を実践に活かしていこうという話です。これらに取り組んでいくためには、他にも学ぶべきことがたくさんあります。例えば、gem を作るのであれば、まず何を作るのか決めなければならないし、Ruby の知識だけでなく設計に関する知識も必要になってきます。
長い道のりになりそうですが、また一歩成長できたなと感じる瞬間を迎えられるよう地道にやっていきたいと思います。

最後にイベントの告知をさせてください。RubyKaigi が終わった後に After RubyKaigi というイベントが開かれます。RubyKaigi にについて語りたい方、また弊社メンバーも参加するので Sansan の Rubyist と話したい方はぜひお越しください。

*1:厳密には、オブジェクトが作成された時点では特異クラスは存在しない(特異メソッドを定義する/特異クラス式を評価する/特異クラスの存在を確認して初めて特異クラスが作成される)のですがわかりやすさのために図示しています。また、Moduleクラス/Classクラス/特異クラスもまた特異クラスを持つのですがわかりやすさのために省略しています。

© Sansan, Inc.