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

クラスレベルのインスタンス変数の魔法

以前のRubyMagicでは、.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
end

Birdの新しいインスタンスを作成して、これを試してみましょう。 make_noiseを呼び出します メソッド。

bird = Bird.new
bird.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise

すごい!期待どおりに動作します。ただし、Wrappableを使用して2番目のクラスを拡張すると、状況が少し奇妙になり始めます。 モジュール。

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 down

Machine Loggingでラップされていません モジュール、それはまだログ情報を出力します。さらに悪いことに、鳥でさえパワーアップとパワーダウンを行っています。それは正しくありませんね

この問題の根本は、モジュールの保存方法にあります。クラス変数@@wrappables 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から継承します 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で拡張されたものを選択できます 。最初にチェーンの上位からのラッパーでインスタンスをラップするため、.reverseを呼び出します。 順序を入れ替えます。

Pigeon.ancestors.grep(Wrappable).reverse # => [Bird, Pigeon]

ルビーの#=== メソッド

Rubyの魔法の一部は#===に帰着します (またはケースの等式 ) 方法。デフォルトでは、#==と同じように動作します (または平等 ) 方法。ただし、いくつかのクラスは#===をオーバーライドします caseで異なる動作を提供するメソッド ステートメント。これが正規表現の使用方法です(#=== #match?と同等です )、またはクラス(#=== #kind_of?と同等です )それらのステートメントで。 Enumerable#grepのようなメソッド 、Enumerable#all? 、またはEnumerable#any? ケース等式法にも依存します。

これで、flat_map(&:wrappers)を呼び出すことができます 継承チェーンで定義されているすべてのラッパーのリストを単一の配列として取得します。

Pigeon.ancestors.grep(Wrappable).reverse.flat_map(&:wrappers) # => [Logging]

残っているのは、それをinherited_wrappersにパックすることだけです。 モジュールを作成し、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にチャープするだけです。


  1. 環境変数へのRubyistsガイド

    開発中および本番環境でWebアプリを効果的に管理できるようにするには、環境変数を理解する必要があります。 これは必ずしもそうではありませんでした。ほんの数年前、環境変数を使用してRailsアプリを構成している人はほとんどいませんでした。しかし、その後、Herokuが発生しました。 Herokuは、開発者に12要素のアプリアプローチを紹介しました。 12要素のアプリマニフェストでは、導入が簡単なアプリを作成するための多くのベストプラクティスを示しています。環境変数に関するセクションは特に影響力があります。 12ファクターのアプリは、構成を環境変数に格納します(多くの場合、env varsま

  2. 字句スコープとRubyクラス変数

    Rubyのクラス変数は紛らわしいです。熟練したRubyユーザーでさえ、直感的に理解するのが難しい場合があります。最も明白な例は、継承と関係があります: class Fruit @@kind = nil def self.kind @@kind end end class Apple < Fruit @@kind = apple end Apple.kind # => apple Fruit.kind # => apple kindの変更 子クラスの変数は、親クラスでも変更します。それはかなりめちゃくちゃです。しかし、これはまさにその言語