Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: ActiveSupport::Concernをextendしたモジュールをprependする機能(翻訳)

概要

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

Rails: ActiveSupport::Concernをextendしたモジュールをprependする機能(翻訳)

Railsにconcernsをprependするためのサポートが追加されました(ba2bea5e)。

prependとは

あるモジュールをクラスにprependすると、そのモジュールは継承チェインの(そのクラス自身よりも手前の)冒頭に挿入される。

Rubyのインスタンスメソッドをprependする場合

module Population
  def preferred_transport
    "by walk"
  end
end

class Human
  prepend Population

  def preferred_transport
    "by air"
  end
end

Human.new.preferred_transport #=> by walk

上のコード例では、PopulationモジュールがHumanクラスでprependされています。

これによって、Populationモジュールにあるpreferred_transportは、探索チェイン内のHumanクラスにある(同名の)preferred_transportよりも優先されるようになりました。

以下のようにHumanクラスでancestorsを呼ぶことでこのことを確認できます。

Human.ancestors
#=> [Population, Human, Object, Kernel, BasicObject]

Rubyのクラスメソッドをprependする場合

module Population
  def self.prepended(base)
    class << base
      prepend ClassMethods
    end
  end

  module ClassMethods
    def count
      5000
    end
  end
end

class Human
  prepend Population

  def self.count
    1000
  end
end

Human.count #=> 5000

上のコード例では、ClassMethodsモジュール内にあるメソッドをprependedフックで拡張し、PopulationモジュールのcountクラスメソッドがHumanクラスの(同名の)countクラスメソッドより優先されるようにしています。

Human.ancestors
#=> [Population, Human, Object, Kernel, BasicObject]

Railsのconcernsをprependする

Railsのconcernsにprependのサポートが追加されたことで、上述の2つのコード例を以下のようにずっとシンプルにまとめられるようになりました。

module Population
  extend ActiveSupport::Concern

  def preferred_transport
    "by walk"
  end

  class_methods do
    def count
      5000
    end
  end
end

class Human
  prepend Population

  def self.count
    1000
  end

  def preferred_transport
    "by air"
  end
end

Human.new.preferred_transport #=> by walk
Human.count #=> 5000

このコード例では、先ほどのコード例のようにprependedフックを用いてクラスメソッドを手動で追加する必要はありません。

Humanクラスの探索チェインをancestorsで調べると以下のような感じになります。

[Population, Human, ActiveSupport::ForkTracker::CoreExtPrivate, ActiveSupport::ForkTracker::CoreExt, ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency, ActiveSupport::ToJsonWithActiveSupportEncoder, Object, PP::ObjectMixin, JSON::Ext::Generator::GeneratorMethods::Object, ActiveSupport::Tryable, ActiveSupport::Dependencies::Loadable, Kernel, BasicObject]

また、以下のようにモジュールがクラスにprependされるときに、prependedブロックを用いて任意のカスタムコードを実行することもできます。

module Population
  extend ActiveSupport::Concern

  prepended do
    puts "the module is prepended"
  end

  def preferred_transport
    "by walk"
  end
end

このprependedブロックは、そのモジュールがクラスにprependされた後で実行されます。これはincludedブロックの動作と似ています。

注意: ひとつのモジュール内にprependedブロックを複数置くことはできません。

prependedブロックを複数宣言しようとするとActiveSupport::Concern::MultiplePrependBlocks例外がraiseされます。


prependconcerningでも動作します。その場合、以下のようにconcerningブロックが始まる直前にprepend: trueを定義する必要があります。

class Human
  concerning :Preferences, prepend: true do
    def preferred_transport
      "by walk"
    end

    class_methods do
      def count
        5000
      end
    end
  end
end

この場合Humanクラスの探索チェインは以下のような感じになります。

 [Human::Preferences, Human, ActiveSupport::ForkTracker::CoreExtPrivate, ActiveSupport::ForkTracker::CoreExt, ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency, ActiveSupport::ToJsonWithActiveSupportEncoder, Object, PP::ObjectMixin, JSON::Ext::Generator::GeneratorMethods::Object, ActiveSupport::Tryable, ActiveSupport::Dependencies::Loadable, Kernel, BasicObject]

関連記事

Rails: 個別のバリデーションエラーをErrorオブジェクトにカプセル化する(翻訳)


CONTACT

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