Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rubyのオブジェクト作成方法を改変する(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Rubyのオブジェクト作成方法を改変する(翻訳)

Rubyの素晴らしさのひとつに、必要に応じてほぼ何でもカスタマイズできるという点があげられます。カスタマイズは便利であると同時に危険も伴うので、うっかりすると簡単に自分の足を撃ち抜いてしまいますが、十分気をつければ相当強力なソリューションを生み出すこともできます。

「Ruby Magic」では、便利さと危険は名コンビであると考えます。それではRubyのオブジェクト初期化方法を調べ、デフォルトの振る舞いを改変してみましょう。

クラスからの新規オブジェクト作成の基本

最初に、Rubyがオブジェクトを作成する方法を見てみましょう。新しいオブジェクト(インスタンス)を作成するには、そのクラスでnewを呼び出します。他の言語と異なり、Rubyのnewは言語のキーワードではなくメソッドであり、次のように他のメソッドとまったく同じように呼び出されます。

class Dog
end

object = Dog.new

このnewメソッドに引数を渡すことで、新しく作成されたオブジェクトをカスタマイズできます。引数として渡したものは、種類を問わずイニシャライザに渡されます。

class Dog
  def initialize(name)
    @name = name
  end
end

object = Dog.new('Good boy')

繰り返しますが、Rubyのイニシャライザは他の言語のような特殊な構文やキーワードではなく、単なるメソッドです。

ということは、Rubyの他のメソッドと同様に、こうしたイニシャライザメソッドにもちょっかいを出せるのではないでしょうか?もちろん可能です!

単独のオブジェクトの振る舞いを改変する

特定のクラスから派生するどのオブジェクトからも、常にログを出力したいとしましょう(メソッドがサブクラスでオーバーライドされたときにもです)。これを実現する方法のひとつは、そのオブジェクトのシングルトンクラスにモジュールを1つ追加することです。

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オブジェクトが1つ作成され、シングルトンクラスを使って、そのオブジェクトにLoggingモジュールがincludeされます。

シングルトンクラスとは

Rubyでは特定のオブジェクトだけで使える固有のメソッドを利用できます。Rubyはこの機能をサポートするために、そのオブジェクトと実際のクラスの間に無名クラスを1つ追加します。メソッドが呼び出されると、実際のクラスにあるメソッドよりも、このシングルトンクラスで定義されているメソッドが優先されます。このシングルトンクラスは他のどのオブジェクトとも異なる固有のものなので、そこにメソッドを追加しても、実際のクラスから派生するオブジェクトには何の影響も生じません。詳しくはProgramming Ruby guideをご覧ください。

オブジェクトが作成されるたびにそのシングルトンクラスをいちいち変更するのはちょっとイケてません。そこでLoggingクラスのincludeをイニシャライザに移して、作成されるすべてのオブジェクトに追加されるようにしましょう。

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を呼ばないと、Loggerクラスはincludeされません。

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モジュールはシングルトンクラスにincludeされないので、結果は期待どおりになりません。

ありがたいことに解決方法がひとつあります。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を呼んでクラスから新規作成されたオブジェクトは初期化されていないので、そこに追加の振る舞いをincludeしてその後でオブジェクトのinitializeメソッドを呼べばよいのです。initializeはデフォルトではprivateメソッドなので、sendを最後の手段として使わなければなりません。

Class#allocateの真実

allocateは他のメソッドとは異なり、オーバーライドは不可能です。Ruby内部ではallocateのメソッドディスパッチで通常の方法を使っていないので、newをオーバーライドせずにallocateをオーバーライドするだけでは動作しません。しかしallocateを直接呼び出してしまえば、再定義されたメソッドが呼び出されます。RubyのClass#newClass#allocateについて詳しくはRubyのドキュメントをご覧ください。

今回改変した理由

Rubyがクラスからオブジェクトを作成する方法を改変することはさまざまな危険を伴いますので、思わぬ部分に影響が生じる可能性があります。

とは言うものの、オブジェクト生成方法の改変に意味のあるユースケースもあります。実際、ActiveRecordでは別のinit_from_dbというメソッドでallocateを用いて初期化プロセスを変更し、保存されてないオブジェクトをビルドするのではなくデータベースからオブジェクトを作成しています。ActiveRecordでは、種類の異なるSTI(Single Table Instance)同士のレコードをbecomesで変換するときにもallocateを使っています。

オブジェクト作成の挙動を変えて遊ぶときに最も重要なのは、Rubyがオブジェクトを作成するときの仕組みを深く理解し、別のソリューションを受け入れることです。本記事を皆さまが楽しんでいただければ幸いです。

皆さまがRubyのデフォルトのオブジェクト作成方法を変更してどんなものを実装したかをお知らせいただけるとうれしく思います。どうぞお気軽に@AppSignalまでお知らせください。

関連記事

Railsのワナ: モデルでbooleanメソッドをオーバーライドするな(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。