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

Rubyのオブジェクト生成を自由にカスタマイズする方法 ― newとallocateの仕組みを深掘り

Rubyの魅力のひとつは、ほぼすべての挙動を自分のニーズに合わせてカスタマイズできる点です。これは非常に便利であると同時に危険でもあります。うっかり足を撃ってしまう(自滅してしまう)ことも簡単に起こり得ますが、慎重に使えば強力なソリューションにつながります。

この記事では、Rubyがどのようにオブジェクトを生成・初期化しているのかを解説し、そのデフォルトの動作をどう変更できるのかを見ていきます。

クラスから新しいオブジェクトを生成する基本

まずは、Rubyでオブジェクトを生成する基本的な方法から確認しましょう。新しいオブジェクト(またはインスタンス)を作成するには、クラスに対してnewを呼び出します。他の多くの言語とは異なり、Rubyのnewは言語のキーワードではなく、他のメソッドと同じように呼び出されるただのメソッドです。

class Dog
end
 
object = Dog.new

新しく作成したオブジェクトをカスタマイズしたい場合は、newメソッドに引数を渡すことができます。渡された引数は、そのままイニシャライザ(initializeメソッド)に引き継がれます。

class Dog
  def initialize(name)
    @name = name
  end
end
 
object = Dog.new('Good boy')

ここでも同様に、Rubyのイニシャライザは特別な構文やキーワードではなく、単なるメソッドです。

つまり、これらのメソッドも他のRubyメソッドと同じように、自由にいじることができるはずです。もちろん、実際にできます!

個別のオブジェクトの振る舞いを変更する

例として、「特定のクラスのすべてのオブジェクトが、サブクラスでメソッドがオーバーライドされていても必ずログを出力する」ようにしたいケースを考えてみましょう。これを実現する方法のひとつが、オブジェクトのシングルトンクラスにモジュールを追加することです。

module Logging
  def make_noise
    puts "Started making noise"
    super
    puts "Finished making noise"
  end
end
 
class Bird
  def make_noise
    puts "Chirp, chirp!"
  end
end
 
object = Bird.new
object.singleton_class.include(Logging)
object.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise

この例では、Bird.newBirdオブジェクトを生成し、そのオブジェクトのシングルトンクラス経由でLoggingモジュールを取り込んでいます。

シングルトンクラスとは何か?

Rubyでは、特定の1つのオブジェクトだけに存在するメソッドを定義できます。これを支えているのがシングルトンクラスです。Rubyはオブジェクトとその実際のクラスの間に、無名のクラス(シングルトンクラス)を挟み込みます。メソッドが呼び出されるとき、シングルトンクラスに定義されたメソッドは、元のクラスのメソッドよりも優先されます。

シングルトンクラスはオブジェクトごとに固有のものであるため、そこにメソッドを追加しても、同じクラスの他のオブジェクトには一切影響しません。クラスとオブジェクトの仕組みについて詳しく知りたい方は、公式ドキュメント「Programming Ruby」などを参照するとよいでしょう。

しかし、オブジェクトを生成するたびに毎回シングルトンクラスを修正するのは少し面倒です。そこで、Loggingモジュールの取り込みをイニシャライザに移動し、生成されるすべてのオブジェクトに自動的に適用されるようにしてみましょう。

module Logging
  def make_noise
    puts "Started making noise"
    super
    puts "Finished making noise"
  end
end
 
class Bird
  def initialize
    singleton_class.include(Logging)
  end
 
  def make_noise
    puts "Chirp, chirp!"
  end
end
 
object = Bird.new
object.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise

これでうまく動きますが、Birdのサブクラス(たとえばDuck)を作ると話が変わります。サブクラスのイニシャライザでsuperを呼び出さない限り、Loggingの振る舞いは失われてしまいます。「メソッドをオーバーライドするときは常に適切にsuperを呼ぶべきだ」という意見もあるかもしれませんが、ここではsuperの呼び出しを必須にしない方法を探してみましょう。

もしサブクラスからsuperを呼ばなければ、Loggingモジュールの取り込みは失われます。

class Duck < Bird
  def initialize(name)
    @name = name
  end
 
  def make_noise
    puts "#{@name}: Quack, quack!"
  end
end
 
object = Duck.new('Felix')
object.make_noise
# Felix: Quack, quack!

そこで今度は、Bird.newそのものをオーバーライドしてみましょう。冒頭で述べたように、newはクラスに実装された単なるメソッドです。つまり、それをオーバーライドし、superを呼び出した上で、新しく生成されたオブジェクトを好きなように加工できるのです。

class Bird
  def self.new(*arguments, &block)
    instance = super
    instance.singleton_class.include(Logging)
    instance
  end
end
 
object = Duck.new('Felix')
object.make_noise
# Started making noise
# Felix: Quack, quack!
# Finished making noise

ところで、イニシャライザの中でmake_noiseを呼び出すとどうなるでしょうか?残念ながら、この時点ではまだシングルトンクラスにLoggingモジュールが取り込まれていないため、期待どおりの出力は得られません。

幸い、解決策があります。allocateを呼び出せば、デフォルトの.newの動作をゼロから再現できるのです。

class Bird
  def self.new(*arguments, &block)
    instance = allocate
    instance.singleton_class.include(Logging)
    instance.send(:initialize, *arguments, &block)
    instance
  end
end

allocateを呼び出すと、そのクラスの「未初期化の」新しいオブジェクトが返されます。その後、追加の振る舞いを取り込み、それから改めてinitializeメソッドをそのオブジェクトに対して呼び出します。(initializeはデフォルトでprivateメソッドであるため、呼び出しにはsendを使う必要があります。)

Class#allocateの真実

他の多くのメソッドとは異なり、allocateはオーバーライドできません。Rubyは内部でallocateに対して通常のメソッドディスパッチの仕組みを使っていないためです。その結果、newも一緒にオーバーライドせずにallocateだけをオーバーライドしても効果はありません。ただし、allocateを直接呼び出した場合には、再定義されたメソッドが呼ばれます。Class#newClass#allocateについて詳しくは、Rubyの公式ドキュメントを参照してください。

なぜこんなことをするのか?

多くのテクニックと同様に、Rubyのオブジェクト生成の仕組みを変更することは危険を伴い、予期しない形で壊れる可能性があります。

それでも、オブジェクト生成を変更することには正当なユースケースが存在します。たとえばActiveRecordは、データベースからオブジェクトを生成する際と、未保存のオブジェクトをビルドする際とで初期化プロセスを切り替えるために、独自のinit_from_dbメソッドとともにallocateを使用しています。また、単一テーブル継承(STI)の異なるタイプ間でレコードを変換するbecomesメソッドでもallocateが活用されています。

何よりも重要なのは、オブジェクト生成をいろいろと試すことで、Rubyにおけるその仕組みへの理解が深まり、問題解決のための視野が広がるという点です。本記事が皆さんの学びの一助となれば幸いです。

Rubyのデフォルトのオブジェクト生成方法を変更して実装したものがあれば、ぜひ教えてください。Twitter(@AppSignal)でのフィードバックもお待ちしています。

  1. Rubyの内部構造に迫る:オブジェクトのメモリレイアウトを徹底解説

    Rubyの内部構造をちょっと覗いてみませんか? それなら、この記事はきっとお役に立ちます。 なぜなら… この記事では、Rubyオブジェクトがメモリ上でどのように配置されているのか、そして内部データ構造を操作してクールなことを実現する方法を、一緒に探検していきます。 シートベルトを締めて、Rubyインタプリタの深淵への旅に出かけましょう! 配列(Array)のメモリレイアウト 配列を作成すると、Rubyはそのデータを保持するためにシステムメモリと、少しのメタデータを確保します。 メタデータには以下が含まれます: 配列のサイズ(要素数) 配列の容量(capacity) クラス情報 オブジェクトの

  2. Rubyのfreezeメソッド完全解説 – オブジェクトの可変性と不変性を理解しよう

    オブジェクトが「変更可能(ミュータブル)」であるとは、どういう意味なのでしょうか? 難しい言葉に構える必要はありません。「可変性(ミュータビリティ)」とは、単純に「オブジェクトの内部状態を後から変更できる」という意味です。これはすべてのオブジェクトのデフォルトの挙動であり、freeze(凍結)されたオブジェクトや、言語側で特別扱いされている一部のオブジェクトだけが例外となります。 つまり、Rubyのすべてのオブジェクトが変更可能というわけではないのです。 なぜ数値やシンボルは変更できないのか? たとえば、整数・シンボル、さらにはtrueやfalse(これらもすべてオブジェクトです)が変化するの