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

ActiveSupports #descendantsメソッド:詳細

RailsはRubyの組み込みオブジェクトに多くのものを追加します。これは、Rubyの「方言」と呼ばれるものであり、Rails開発者が1.day.agoのような行を記述できるようにするものです。 。

これらの追加のメソッドのほとんどは、ActiveSupportに存在します。今日は、ActiveSupportがクラスに直接追加するあまり知られていないメソッドdescendantsを見ていきます。 。このメソッドは、呼び出されたクラスのすべてのサブクラスを返します。例:ApplicationRecord.descendants それを継承するアプリ内のクラス(たとえば、アプリケーション内のすべてのモデル)を返します。この記事では、それがどのように機能するか、なぜそれを使用したいか、そしてそれがRubyの組み込みの継承関連メソッドをどのように拡張するかを見ていきます。

オブジェクト指向言語での継承

まず、Rubyの継承モデルについて簡単に復習します。他のオブジェクト指向(OO)言語と同様に、Rubyは階層内にあるオブジェクトを使用します。クラスを作成し、次にそのクラスのサブクラスを作成し、次にそのサブクラスのサブクラスを作成することができます。この階層を上っていくと、祖先のリストが表示されます。 Rubyには、すべてという優れた機能もあります。 エンティティはオブジェクト自体(クラス、整数、さらにはnilを含む)ですが、他の一部の言語では、通常はパフォーマンスのために、真のオブジェクトではない「プリミティブ」を使用することがよくあります(整数、倍精度浮動小数点数、ブール値など。I ' mあなたを見ている、Java)。

Rubyと、実際、すべてのオブジェクト指向言語は、メソッドを検索する場所と優先されるメソッドを認識できるように、祖先を追跡する必要があります。

class BaseClass
  def base
    "base"
  end

  def overridden
    "Base"
  end
end

class SubClass < BaseClass
  def overridden
    "Subclass"
  end
end

ここでは、SubClass.new.overriddenを呼び出します "SubClass"を提供します 。ただし、SubClass.new.base はサブクラス定義に存在しないため、Rubyは各祖先を調べて、どの祖先がメソッドを実装しているかを確認します(存在する場合)。 SubClass.ancestorsを呼び出すだけで、祖先のリストを確認できます。 。 Railsでは、結果は次のようになります。

[SubClass,
 BaseClass,
 ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency,
 ActiveSupport::ToJsonWithActiveSupportEncoder,
 Object,
 PP::ObjectMixin,
 JSON::Ext::Generator::GeneratorMethods::Object,
 ActiveSupport::Tryable,
 ActiveSupport::Dependencies::Loadable,
 Kernel,
 BasicObject]

ここでは、このリスト全体を分析しません。私たちの目的では、SubClassに注意するだけで十分です。 一番上にあり、BaseClass その下。また、BasicObjectにも注意してください 一番下にあります。これはRubyの最上位オブジェクトであるため、常にスタックの最下位になります。

モジュール(別名「ミックスイン」)

モジュールをミックスに追加すると、状況は少し複雑になります。モジュールはクラス階層の祖先ではありませんが、クラスに「含める」ことができるため、Rubyは、モジュールのメソッドをいつチェックするか、または複数のモジュールが含まれている場合はどのモジュールを最初にチェックするかを知る必要があります。 。

一部の言語では、この種の「多重継承」は許可されていませんが、Rubyは、モジュールを含めるか追加するかによって、モジュールを階層のどこに挿入するかを選択できるようにすることで、さらに一歩進んでいます。

プレペンディングモジュール

接頭辞付きのモジュールは、その名前が示すように、クラスの前にリストの祖先に挿入され、基本的にクラスのメソッドのいずれかをオーバーライドします。これは、先頭に追加されたモジュールのメソッドで「super」を呼び出して、元のクラスのメソッドを呼び出すことができることも意味します。

module PrependedModule
  def test
    "module"
  end

  def super_test
    super
  end
end

# Re-using `BaseClass` from earlier
class SubClass < BaseClass
  prepend PrependedModule

  def test
    "Subclass"
  end

  def super_test
    "Super calls SubClass"
  end
end

サブクラスの祖先は次のようになります:

[PrependedModule,
 SubClass,
 BaseClass,
 ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency,
 ...
]

この新しい祖先のリストを使用して、PrependedModule はファーストインラインになりました。つまり、RubyはSubClassで呼び出すメソッドを最初に検索します。 。これも つまり、superを呼び出すと、 PrependedModule内 、SubClassでメソッドを呼び出します :

> SubClass.new.test
=> "module"
> SubClass.new.super_test
=> "Super calls SubClass"

モジュールを含む

一方、含まれているモジュールは、の祖先に挿入されます。 クラス。これにより、基本クラスで処理されるメソッドをインターセプトするのに理想的です。

class BaseClass
  def super_test
    "Super calls base class"
  end
end

module IncludedModule
  def test
    "module"
  end

  def super_test
    super
  end
end

class SubClass < BaseClass
  include IncludedModule

  def test
    "Subclass"
  end
end

この配置により、サブクラスの祖先は次のようになります。

[SubClass,
 IncludedModule,
 BaseClass,
 ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency,
 ...
]

現在、SubClassが最初の呼び出しポイントであるため、RubyはIncludedModuleのメソッドのみを実行します。 SubClassに存在しない場合 。 superは 、superへの呼び出し SubClassIncludedModuleに移動します まず、superへの呼び出し IncludedModule BaseClassに移動します 。

言い換えると、インクルードされたモジュールは、祖先階層のサブクラスとその基本クラスの間に位置します。これは事実上、基本クラスによって処理されるメソッドを「インターセプト」するために使用できることを意味します。

> SubClass.new.test
=> "Subclass"
> SubClass.new.super_test
=> "Super calls BaseClass"

この「コマンドのチェーン」のために、Rubyはクラスの祖先を追跡する必要があります。ただし、その逆は当てはまりません。特定のクラスが与えられた場合、Rubyはその子、つまり「子孫」を追跡する必要はありません。メソッドを実行するためにこの情報が必要になることはないからです。

祖先の注文

賢明な読者は、クラスで複数のモジュールを使用している場合、それらを含める(または追加する)順序によって異なる結果が生じる可能性があることに気付いたかもしれません。たとえば、メソッドに応じて、このクラスは次のようになります。

class SubClass < BaseClass
  include IncludedModule
  include IncludedOtherModule
end

そしてこのクラス:

class SubClass < BaseClass
  include IncludedOtherModule
  include IncludedModule
end

まったく異なる動作をする可能性があります。これらの2つのモジュールに同じ名前のメソッドがある場合、ここでの順序によって、どちらが優先されるかが決まります superへの呼び出し に解決されます。個人的には、このようにメソッドが重複することはできるだけ避けたいと思います。特に、モジュールが含まれている順序などについて心配する必要がないようにするためです。

実際の使用法

includeの違いを知っておくのは良いことですが およびprepend モジュールの場合、より現実的な例が、どちらを選択するかを示すのに役立つと思います。このようなモジュールの私の主なユースケースは、Railsエンジンを使用する場合です。

おそらく最も人気のあるRailsエンジンの1つはdeviseです。使用されているパスワードダイジェストアルゴリズムを変更したいとしますが、最初に、簡単な免責事項:

私が日常的にモジュールを使用しているのは、デフォルトのビジネスロジックを保持するRailsエンジンの動作をカスタマイズすることです。 制御するコードの動作をオーバーライドします 。もちろん、Rubyのどの部分にも同じ方法を適用できますが、 外部コードへの変更は変更と互換性がない可能性があるため、制御していないコード(他の人が管理しているgemなど)をオーバーライドすることはお勧めしません。

Deviseのパスワードダイジェストは、ここでDevise ::Models ::DatabaseAuthenticatableモジュールで行われます:

  def password_digest(password)
    Devise::Encryptor.digest(self.class, password)
  end

  # and also in the password check:
  def valid_password?(password)
    Devise::Encryptor.compare(self.class, encrypted_password, password)
  end

Deviseでは、独自のDevise::Encryptable::Encryptorsを作成することにより、ここで使用されているアルゴリズムをカスタマイズできます。 、これは正しい方法です。ただし、デモンストレーションの目的で、モジュールを使用します。

# app/models/password_digest_module
module PasswordDigestModule
  def password_digest(password)
    # Devise's default bcrypt is better for passwords,
    # using sha1 here just for demonstration
    Digest::SHA1.hexdigest(password)
  end

  def valid_password?(password)
    Devise.secure_compare(password_digest(password), self.encrypted_password)
  end
end

begin
  User.include(PasswordDigestModule)
# Pro-tip - because we are calling User here, ActiveRecord will
# try to read from the database when this class is loaded.
# This can cause commands like `rails db:create` to fail.
rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
end

このモジュールをロードするには、Rails.application.eager_load!を呼び出す必要があります。 開発中、またはRails初期化子を追加してファイルをロードします。テストすることで、期待どおりに機能することがわかります。

> User.create!(email: "one@test.com", name: "Test", password: "TestPassword")
=> #<User id: 1, name: "Test", created_at: "2021-05-01 02:08:29", updated_at: "2021-05-01 02:08:29", posts_count: nil, email: "one@test.com">
> User.first.valid_password?("TestPassword")
=> true
> User.first.encrypted_password
=> "4203189099774a965101b90b74f1d842fc80bf91"

この例では、両方のinclude およびprepend 同じ結果になりますが、複雑さを追加しましょう。ユーザーモデルが独自のpassword_saltを実装している場合はどうなりますか メソッドですが、モジュールメソッドでオーバーライドします:

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_many :posts

  def password_salt
    # Terrible way to create a password salt,
    # purely for demonstration purposes
    Base64.encode64(email)[0..-4]
  end
end

次に、独自のpassword_saltを使用するようにモジュールを更新します パスワードダイジェストを作成するときの方法:

  def password_digest(password)
    # Devise's default bcrypt is better for passwords,
    # using sha1 here just for demonstration
    Digest::SHA1.hexdigest(password + "." + password_salt)
  end

  def password_salt
    # an even worse way of generating a password salt
    "salt"
  end

さて、include およびprepend どちらを使用するかによって、どのpassword_saltが決まるため、動作が異なります。 Rubyが実行するメソッド。 prependを使用 、モジュールが優先され、次のようになります:

> User.last.password_digest("test")
=> "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.salt"

includeを使用するようにモジュールを変更する 代わりに、Userクラスの実装が優先されることを意味します:

> User.last.password_digest("test")
=> "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.dHdvQHRlc3QuY2"

通常、私はprependに手を伸ばします まず、モジュールを作成するときに、サブクラスのように扱いやすく、モジュール内のメソッドがクラスのバージョンをオーバーライドすると想定しているためです。明らかに、これは常に望ましいとは限りません。そのため、Rubyはincludeも提供します。 オプション。

子孫

Rubyがクラスの祖先を追跡して、メソッドを実行するときの優先順位を知る方法と、モジュールを介してこのリストにエントリを挿入する方法を見てきました。ただし、プログラマーとしては、クラスのすべての子孫を反復処理すると便利な場合があります。 、 それも。これは、ActiveSupportの#descendantsが存在する場所です メソッドが登場します。メソッドは非常に短く、必要に応じてRailsの外部で簡単に複製できます。

class Class
  def descendants
    ObjectSpace.each_object(singleton_class).reject do |k|
      k.singleton_class? || k == self
    end
  end
end

ObjectSpaceは、現在メモリにあるすべてのRubyオブジェクトに関する情報を格納するRubyの非常に興味深い部分です。ここでは詳しく説明しませんが、アプリケーションでクラスが定義されている(そしてロードされている)場合は、ObjectSpaceに存在します。 ObjectSpace#each_object 、モジュールが渡されると、モジュールに一致するか、モジュールのサブクラスであるオブジェクトのみを返します。ここのブロックはトップレベルも拒否します(たとえば、Numeric.descendantsを呼び出す場合 、Numericは期待していません 結果に含まれます。

ここで何が起こっているのかを完全に理解していなくても心配しないでください。実際に理解するには、ObjectSpaceについてさらに読む必要があるでしょう。私たちの目的では、このメソッドがClassに存在することを知っていれば十分です。 子孫クラスのリストを返します。または、そのクラスの子、孫などの「家系図」と考えることもできます。

#descendantsの実際の使用

2018 RailsConfで、RyanLaughlinが「健康診断」について講演しました。このビデオは一見の価値がありますが、データベース内のすべての行を定期的に実行し、モデルの有効性チェックに合格するかどうかを確認するという1つのアイデアを抽出します。データベース内の行数が#valid?を通過しないことに驚かれるかもしれません。 テスト。

では、問題は、モデルのリストを手動で維持することなく、このチェックをどのように実装するかということです。 #descendants 答えは:

# Ensure all models are loaded (should not be necessary in production)
Rails.application.load! if Rails.env.development?

ApplicationRecord.descendants.each do |model_class|
  # in the real world you'd want to send this off to background job(s)
  model_class.all.each do |record|
    if !record.valid?
      HoneyBadger.notify("Invalid #{model.name} found with ID: #{record.id}")
    end
  end
end

ここでは、ApplicationRecord.descendants 標準のRailsアプリケーションのすべてのモデルのリストを提供します。ループでは、model クラスです(例:User またはProduct )。ここでの実装は非常に基本的ですが、結果として、すべてのモデル(より正確には、ApplicationRecordのすべてのサブクラス)を反復処理し、.valid?を呼び出します。 すべての行に対して。

結論

ほとんどのRails開発者にとって、モジュールは一般的に使用されていません。これには正当な理由があります。コードを所有している場合は、通常、その動作をカスタマイズする簡単な方法があります。しない コードを所有している場合、モジュールを使用してその動作を変更することにはリスクがあります。それにもかかわらず、それらにはユースケースがあり、クラスを別のファイルから変更できるだけでなく、場所を選択するオプションもあるというRubyの柔軟性の証です。 祖先チェーンにモジュールが表示されます。

次に、ActiveSupportが入り、#ancestorsの逆を提供します。 #descendantsを使用 。私が見た限りでは、この方法はめったに使用されませんが、そこにあることがわかったら、おそらくますます多くの用途が見つかるでしょう。個人的には、モデルの有効性をチェックするためだけでなく、attribute_aliasが正しく追加されていることを検証するための仕様でも使用しました。 すべてのモデルのメソッド。


  1. Rubyエイリアスキーワードの使用方法

    Rubyメソッドに別の名前を付けるには、次の2つの方法があります。 エイリアス(キーワード) alias_method 彼らはわずかに異なる方法で同じことをするので、これは紛らわしいトピックになる可能性があります。 この画像は違いの要約です : しっかりと理解するために、これらの違いをさらに詳しく調べてみましょう! エイリアスキーワード まず、aliasがあります 、これはRubyキーワードです(ifなど) 、def 、class 、など) このように見えます : alias print_something puts print_something 1 prin

  2. Ruby Freezeメソッド–オブジェクトの可変性を理解する

    オブジェクトが可変であるとはどういう意味ですか? 派手な言葉で混乱させないでください。「可変性 」は、オブジェクトの内部状態を変更できることを意味します。これは、凍結されたオブジェクトを除く、すべてのオブジェクトのデフォルトです。 、または特別なオブジェクトのリストの一部であるもの。 つまり、Rubyのすべてのオブジェクトが変更可能というわけではありません! 例 : 数字や記号、さらにはtrueには意味がありません またはfalse (オブジェクトでもあります)変更します。 数字の1は常に1になります。 ただし、他のオブジェクト、特に配列オブジェクトやハッシュオブジェクトなどのデー