Rubyのクラスレベル・インスタンス変数の魔法 ― 再利用可能なWrappableモジュールを作る
前回のRuby Magicでは、.newメソッドをオーバーライドすることでクラスに確実にモジュールを注入し、メソッドを追加の振る舞いで包む(ラップする)方法を解説しました。
今回はその一歩先へ進みます。この振る舞いを独立したモジュールとして切り出し、再利用できる形にしていきます。Wrappableモジュールを作成してクラス拡張を自動的に処理できるようにしながら、その過程で「クラスレベルのインスタンス変数」について徹底的に学んでいきましょう。それでは早速始めます!
Wrappableモジュールのご紹介
オブジェクトが初期化される際にモジュールでラップするには、どのモジュールを使うのかを事前にクラスへ伝えておく必要があります。まずはシンプルなWrappableモジュールから作り始めましょう。このモジュールは、指定されたモジュールをクラス属性として定義された配列に追加するwrapメソッドを提供します。加えて、前回の記事で説明したとおり、newメソッドも再定義します。
module Wrappable
@@wrappers = []
def wrap(mod)
@@wrappers << mod
end
def new(*arguments, &block)
instance = allocate
@@wrappers.each { |mod| instance.singleton_class.include(mod) }
instance.send(:initialize, *arguments, &block)
instance
end
endクラスに新しい振る舞いを追加するには、extendを使用します。extendメソッドは指定されたモジュールをクラスに取り込み、そのメソッドをクラスメソッドとして使えるようにします。これで、このクラスのインスタンスをラップするモジュールを登録するために、wrapメソッドを呼び出せるようになりました。
module Logging
def make_noise
puts "Started making noise"
super
puts "Finished making noise"
end
end
class Bird
extend Wrappable
wrap Logging
def make_noise
puts "Chirp, chirp!"
end
endBirdの新しいインスタンスを作成し、make_noiseメソッドを呼び出して試してみましょう。
bird = Bird.new
bird.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise完璧です!期待どおりに動作しています。しかし、別のクラスにもWrappableモジュールをextendすると、奇妙な挙動が現れ始めます。
module Powered
def make_noise
puts "Powering up"
super
puts "Shutting down"
end
end
class Machine
extend Wrappable
wrap Powered
def make_noise
puts "Buzzzzzz"
end
end
machine = Machine.new
machine.make_noise
# Powering up
# Started making noise
# Buzzzzzz
# Finished making noise
# Shutting down
bird = Bird.new
bird.make_noise
# Powering up
# Started making noise
# Chirp, chirp!
# Finished making noise
# Shutting downMachineはLoggingモジュールでラップされていないはずなのに、ログ情報が出力されてしまっています。さらに悪いことに、鳥まで起動・シャットダウンするようになってしまいました。これは明らかにおかしいですね。
この問題の根本原因は、モジュールの保存方法にあります。クラス変数@@wrappersはWrappableモジュール上に定義されているため、wrapがどのクラス内で使われたかに関係なく、常に同じ配列が使われてしまうのです。
WrappableモジュールとBird・Machineクラスそれぞれに定義されたクラス変数を見ると、この問題がより明白になります。Wrappable側にはクラス変数が定義されている一方、2つのクラス側には存在しないのです。
Wrappable.class_variables # => [:@@wrappers]
Bird.class_variables # => []
Machine.class_variables # => []これを修正するには、実装をインスタンス変数を使う形に変更する必要があります。ただし、ここでのインスタンス変数はBirdやMachineのインスタンスに対するものではなく、クラスそのものに対するインスタンス変数です。
Rubyにおいて、クラスはただのオブジェクト
最初は少し頭が混乱するかもしれませんが、非常に重要な概念なのでぜひ理解しておきましょう。クラスはClassのインスタンスであり、class Bird; endと書くことはBird = Class.newと書くことと同等です。さらにややこしいことに、ClassはModuleを継承し、ModuleはObjectを継承しています。その結果、クラスやモジュールは他のあらゆるオブジェクトと同じメソッドを持つことになります。私たちがクラスに対してよく使うメソッド(attr_accessorマクロなど)の多くは、実際にはModuleのインスタンスメソッドなのです。
クラスへのインスタンス変数の使用
それでは、Wrappableの実装をインスタンス変数を使う形に変更しましょう。コードをすっきりさせるため、インスタンス変数がまだ存在しない場合は配列を新しくセットアップし、既に存在すればそれを返すwrappersメソッドを導入します。併せて、wrapメソッドとnewメソッドも、この新しいメソッドを利用するように修正します。
module Wrappable
def wrap(mod)
wrappers << mod
end
def wrappers
@wrappers ||= []
end
def new(*arguments, &block)
instance = allocate
wrappers.each { |mod| instance.singleton_class.include(mod) }
instance.send(:initialize, *arguments, &block)
instance
end
endモジュールと2つのクラスのインスタンス変数を確認すると、BirdとMachineの両方が、それぞれ独自のラッピングモジュールのコレクションを保持していることがわかります。
Wrappable.instance_variables #=> []
Bird.instance_variables #=> [:@wrappers]
Machine.instance_variables #=> [:@wrappers]当然ながら、これにより先ほどの問題も解決しました。各クラスは自分専用のモジュールだけでラップされるようになっています。
bird = Bird.new
bird.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise
machine = Machine.new
machine.make_noise
# Powering up
# Buzzzzzz
# Shutting down継承のサポート
ここまでは順調ですが、継承が絡んでくると話が変わります。本来なら、サブクラスはスーパークラスのラッピングモジュールを引き継ぐべきですよね。実際に確認してみましょう。
module Flying
def make_noise
super
puts "Is flying away"
end
end
class Pigeon < Bird
wrap Flying
def make_noise
puts "Coo!"
end
end
pigeon = Pigeon.new
pigeon.make_noise
# Coo!
# Is flying awayご覧のとおり、期待どおりには動作しません。Pigeonもまた、独自のラッピングモジュールのコレクションを保持してしまっているからです。Pigeon向けに定義したモジュールがBirdには反映されないのは理にかなっていますが、これでは私たちの望む挙動ではありません。継承チェーン全体からすべてのラッパーを取得する方法を考えましょう。
幸い、RubyにはModule#ancestorsというメソッドがあり、クラス(またはモジュール)が継承しているすべてのクラスとモジュールを一覧できます。
Pigeon.ancestors # => [Pigeon, Bird, Object, Kernel, BasicObject]ここにgrepを組み合わせれば、実際にWrappableでextendされているものだけを抽出できます。チェーンの上位にあるラッパーから先に適用したいので、.reverseを呼び出して順序を反転させます。
Pigeon.ancestors.grep(Wrappable).reverse # => [Bird, Pigeon]Rubyの#===メソッド
Rubyの魔法の一部は、#===(いわゆるcase等価性)メソッドによるものです。デフォルトでは、このメソッドは#==(等価性)メソッドと同じように振る舞います。しかし、いくつかのクラスはcase文の中で異なる挙動を提供するために#===をオーバーライドしています。これにより、case文の中で正規表現(#===は#match?と同等)やクラス(#===は#kind_of?と同等)を使えるようになっているのです。Enumerable#grep、Enumerable#all?、Enumerable#any?といったメソッドも、このcase等価性メソッドに依存しています。
次に、flat_map(&:wrappers)を呼び出せば、継承チェーン上に定義されたすべてのラッパーを1つの配列として取得できます。
Pigeon.ancestors.grep(Wrappable).reverse.flat_map(&:wrappers) # => [Logging]あとはこれをinherited_wrappersメソッドとしてまとめ、newメソッドを少し修正してwrappersの代わりにこのメソッドを使うようにするだけです。
module Wrappable
def inherited_wrappers
ancestors
.grep(Wrappable)
.reverse
.flat_map(&:wrappers)
end
def new(*arguments, &block)
instance = allocate
inherited_wrappers.each { |mod|instance.singleton_class.include(mod) }
instance.send(:initialize, *arguments, &block)
instance
end
end最終テストを実行すると、すべてが期待どおりに動作していることが確認できます。ラッピングモジュールは、適用対象のクラス(およびそのサブクラス)にのみ適用されます。
bird = Bird.new
bird.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise
machine = Machine.new
machine.make_noise
# Powering up
# Buzzzzz
# Shutting down
pigeon = Pigeon.new
pigeon.make_noise
# Started making noise
# Coo!
# Finished making noise
# Is flying awayまとめ ― ラップ完了!
正直に言えば、この騒がしい鳥たちの例はやや理論的すぎるかもしれません(ピヨピヨ)。しかし、継承可能なクラスレベルのインスタンス変数は、クラスの仕組みを理解するうえで面白いだけでなく、「Rubyにおいてクラスはただのオブジェクトである」ということを示す絶好の例でもあります。
そして認めざるを得ませんが、継承可能なクラスレベルのインスタンス変数は、実務でも十分に役立つ可能性があります。たとえば、モデルに属性やリレーションを定義し、後からそれらをイントロスペクト(内省)できるようにするようなケースを想像してみてください。私たちにとっての魔法とは、こうした仕組みに触れて遊びながら、物事の動作原理への理解を深めることです。そして、ワンランク上のソリューションへと発想を広げていくことなのです。🧙🏼♀️
いつものように、このパターンや類似のパターンを使って何を作ったか、ぜひ聞かせてください。Twitterで@AppSignalにピヨッと声をかけてくださいね。
-
Rubyistのための環境変数徹底ガイド――仕組みの理解からRailsでの実践管理まで
Webアプリを開発環境と本番環境の両方で効果的に管理したいなら、環境変数への深い理解は欠かせません。 実は、これは昔から当たり前だったわけではありません。数年前まで、Railsアプリを環境変数で設定する開発者はほとんどいませんでした。しかし、Herokuの登場がその常識を大きく変えたのです。 Herokuは開発者に「12-Factor App(12ファクター・アプリ)」という設計手法を広めました。このマニフェストには、デプロイしやすいアプリを作るためのベストプラクティスが数多くまとめられており、中でも環境変数に関するセクションは特に大きな影響力を持っています。 12ファクター・アプリは設定(c
-
Rubyクラス変数の罠――字句スコープが引き起こす予期せぬ挙動
Rubyのクラス変数(@@variable)は紛らわしいことで有名です。熟練したRuby開発者であっても、その挙動を直感的に理解するのは容易ではありません。最もわかりやすい例が、継承に関するものです。 class Fruit @@kind = nil def self.kind @@kind end end class Apple < Fruit @@kind = apple end Apple.kind # => apple Fruit.kind # => apple 子クラスでkindを変更すると、なんと親クラスの値まで変わってしまう