概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Changing the Way Ruby Creates Objects | AppSignal Blog
- 原文公開日: 2018/08/07
- 著者: Benedikt Deicke(@benediktdeicke) -- Userlist.ioのCTOであり書籍『SaaS applications in Ruby on Rails』の著者です。
- サイト: AppSignal
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.new
でBird
オブジェクトが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#new
やClass#allocate
について詳しくはRubyのドキュメントをご覧ください。
今回改変した理由
Rubyがクラスからオブジェクトを作成する方法を改変することはさまざまな危険を伴いますので、思わぬ部分に影響が生じる可能性があります。
とは言うものの、オブジェクト生成方法の改変に意味のあるユースケースもあります。実際、ActiveRecordでは別のinit_from_db
というメソッドでallocate
を用いて初期化プロセスを変更し、保存されてないオブジェクトをビルドするのではなくデータベースからオブジェクトを作成しています。ActiveRecordでは、種類の異なるSTI(Single Table Instance)同士のレコードをbecomes
で変換するときにもallocate
を使っています。
オブジェクト作成の挙動を変えて遊ぶときに最も重要なのは、Rubyがオブジェクトを作成するときの仕組みを深く理解し、別のソリューションを受け入れることです。本記事を皆さまが楽しんでいただければ幸いです。
皆さまがRubyのデフォルトのオブジェクト作成方法を変更してどんなものを実装したかをお知らせいただけるとうれしく思います。どうぞお気軽に@AppSignalまでお知らせください。