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

構成可能なRubyモジュール:モジュールビルダーパターン

この投稿では、コードのユーザーが構成できるRubyモジュールを作成する方法について説明します。これは、gemの作成者がライブラリに柔軟性を追加できるようにするパターンです。

ほとんどのRuby開発者は、モジュールを使用して動作を共有することに精通しています。結局のところ、ドキュメントによると、これは彼らの主なユースケースの1つです:

モジュールは、Rubyで名前空間とミックスイン機能の2つの目的を果たします。

Railsは、ActiveSupport::Concernの形式でいくつかのシンタックスシュガーを追加しました 、ただし、一般的な原則は同じです。

問題

モジュールを使用してミックスイン機能を提供するのは通常簡単です。私たちがしなければならないのは、いくつかのメソッドをバンドルし、モジュールを他の場所に含めることだけです:

module HelloWorld
  def hello
    "Hello, world!"
  end
end
class Test
  include HelloWorld
end
Test.new.hello
#=> "Hello, world!"

これはかなり静的なメカニズムですが、Rubyのinherited およびextended フックメソッドは、インクルードクラスに基づいていくつかのさまざまな動作を可能にします:

module HelloWorld
  def self.included(base)
    define_method :hello do
      "Hello, world from #{base}!"
    end
  end
end
class Test
  include HelloWorld
end
Test.new.hello
#=> "Hello, world from Test!"

これはやや動的ですが、コードユーザーがたとえばhelloの名前を変更することはできません。 モジュール包含時のメソッド。

ソリューション:構成可能なRubyモジュール

過去数年の間に、この問題を解決する新しいパターンが出現しました。これは、「モジュールビルダーパターン」と呼ばれることもあります。この手法は、Rubyの2つの主要な機能に依存しています。

  • モジュールは他のオブジェクトとまったく同じです。モジュールはその場で作成したり、変数に割り当てたり、動的に変更したり、メソッドに渡したり、メソッドから返したりすることができます。

    def make_module
      # create a module on the fly and assign it to variable
      mod = Module.new
     
      # modify module
      mod.module_eval do
        def hello
          "Hello, AppSignal world!"
        end
      end
     
      # explicitly return it
      mod
    end
  • includeへの引数 またはextend 呼び出しはモジュールである必要はありません。モジュールを返す式にすることもできます。メソッド呼び出し。

    class Test
      # include the module returned by make_module
      include make_module
    end
     
    Test.new.hello
    #=> "Hello, AppSignal world!"

動作中のモジュールビルダー

この知識を使用して、Wrapperという単純なモジュールを作成します。 、次の動作を実装します:

  1. Wrapperを含むクラス 特定のタイプのオブジェクトのみをラップできます。コンストラクターは引数の型を検証し、型が期待されるものと一致しない場合はエラーを発生させます。
  2. ラップされたオブジェクトは、original_<class>というインスタンスメソッドを介して利用できます。 、例: original_integer またはoriginal_string
  3. これにより、コードのコンシューマーは、このアクセサーメソッドの代替名(the_stringなど)を指定できるようになります。 。

コードの動作を見てみましょう:

# 1
class IntWrapper
 # 2
 include Wrapper.for(Integer)
end
 
# 3
i = IntWrapper.new(42)
i.original_integer
#=> 42
 
# 4
i = IntWrapper.new("42")
#=> TypeError (not a Integer)
 
# 5
class StringWrapper
 include Wrapper.for(String, accessor_name: :the_string)
end
 
s = StringWrapper.new("Hello, World!")
# 6
s.the_string
#=> "Hello, World!"

ステップ1では、IntWrapperという新しいクラスを定義します。 。

ステップ2では、このクラスに名前でモジュールが含まれているだけでなく、Wrapper.for(Integer)の呼び出しの結果が混在していることを確認します。 。

ステップ3では、新しいクラスのオブジェクトをインスタンス化し、それをiに割り当てます。 。指定されているように、このオブジェクトにはoriginal_integerというメソッドがあります 、それは私たちの要件の1つを満たしています。

手順4で、文字列などの間違った型の引数を渡そうとすると、役立つTypeError 発生します。最後に、ユーザーがカスタムアクセサー名を指定できることを確認しましょう。

このために、StringWrapperという新しいクラスを定義します。 手順5で、the_stringを渡します キーワード引数としてaccessor_name 、ステップ6で実際に動作していることがわかります。

これは確かにやや不自然な例ですが、モジュールビルダーのパターンとその使用方法を示すのに十分なさまざまな動作があります。

最初の試み

要件と使用例に基づいて、実装を開始できます。 Wrapperという名前のモジュールが必要であることはすでにわかっています。 forと呼ばれるモジュールレベルのメソッドを使用します 、オプションのキーワード引数としてクラスを取ります:

module Wrapper
 def self.for(klass, accessor_name: nil)
 end
end

このメソッドの戻り値はincludeの引数になるため 、モジュールである必要があります。したがって、Module.newを使用して新しい匿名のものを作成できます。 。

Module.new do
end

要件に従って、これは、渡されたオブジェクトのタイプを検証するコンストラクターと、適切な名前のアクセサーメソッドを定義する必要があります。コンストラクターから始めましょう:

define_method :initialize do |object|
 raise TypeError, "not a #{klass}" unless object.is_a?(klass)
 @object = object
end

このコードはdefine_methodを使用しています インスタンスメソッドをレシーバーに動的に追加します。ブロックはクロージャーとして機能するため、klassを使用できます 必要なタイプチェックを実行するための外部スコープからのオブジェクト。

適切な名前のアクセサメソッドを追加することはそれほど難しくありません:

# 1
method_name = accessor_name || begin
 klass_name = klass.to_s.gsub(/(.)([A-Z])/,'\1_\2').downcase
 "original_#{klass_name}"
end
 
# 2
define_method(method_name) { @object }

まず、コードの呼び出し元がaccessor_nameを渡したかどうかを確認する必要があります 。その場合は、それをmethod_nameに割り当てるだけです。 その後、完了します。それ以外の場合は、クラスを取得して、下線付きの文字列(たとえば、Integer)に変換します。 integerに変わります またはOpenStruct open_structに 。このklass_name 次に、変数の前にoriginal_が付けられます 最終的なアクセサ名を生成します。メソッドの名前がわかったら、再びdefine_methodを使用します 手順2に示すように、モジュールに追加します。

これまでの完全なコードは次のとおりです。柔軟で構成可能なRubyモジュールの場合は20行未満。悪くない。

module Wrapper
  def self.for(klass, accessor_name: nil)
    Module.new do
      define_method :initialize do |object|
        raise TypeError, "not a #{klass}" unless object.is_a?(klass)
        @object = object
      end
 
      method_name = accessor_name || begin
        klass_name = klass.to_s.gsub(/(.)([A-Z])/,'\1_\2').downcase
        "original_#{klass_name}"
      end
 
      define_method(method_name) { @object }
    end
  end
end

注意深い読者は、Wrapper.forを覚えているかもしれません。 匿名モジュールを返します。これは問題ではありませんが、オブジェクトの継承チェーンを調べるときに少し混乱する可能性があります:

StringWrapper.ancestors
#=> [StringWrapper, #<Module:0x0000000107283680>, Object, Kernel, BasicObject]

ここで#<Module:0x0000000107283680> (名前はあなたがフォローしている場合は異なります)私たちの匿名モジュールを指します。

改良版

匿名モジュールの代わりに名前付きモジュールを返すことで、ユーザーの生活を楽にしましょう。このためのコードは、以前のものと非常に似ていますが、いくつかの小さな変更があります。

module Wrapper
  def self.for(klass, accessor_name: nil)
    # 1
    mod = const_set("#{klass}InstanceMethods", Module.new)
 
    # 2
    mod.module_eval do
      define_method :initialize do |object|
        raise TypeError, "not a #{klass}" unless object.is_a?(klass)
        @object = object
      end
 
      method_name = accessor_name || begin
        klass_name = klass.to_s.gsub(/(.)([A-Z])/, '\1_\2').downcase
        "original_#{klass_name}"
      end
 
      define_method(method_name) { @object }
    end
 
    # 3
    mod
  end
end

最初のステップでは、「#{klass} InstanceMethods」というネストされたモジュールを作成します(たとえば、IntegerInstanceMethods )、それは単なる「空の」モジュールです。

手順2に示すように、module_evalを使用します for メソッド。呼び出されたモジュールのコンテキストでコードのブロックを評価します。このようにして、ステップ3でモジュールを返す前に、モジュールに動作を追加できます。

ここで、Wrapperを含むクラスの祖先を調べます。 、出力には適切な名前のモジュールが含まれます。これは、以前の匿名モジュールよりもはるかに意味があり、デバッグが簡単です。

StringWrapper.ancestors
#=> [StringWrapper, Wrapper::StringInstanceMethods, Object, Kernel, BasicObject]

実際のモジュールビルダーパターン

この投稿とは別に、モジュールビルダーパターンまたは同様の手法を他にどこで見つけることができますか?

一例はdry-rbです。 宝石のファミリー。たとえば、dry-effects モジュールビルダーを使用して、構成オプションをさまざまなエフェクトハンドラーに渡します。

# This adds a `counter` effect provider. It will handle (eliminate) effects
include Dry::Effects::Handler.State(:counter)
 
# Providing scope is required
# All cache values will be scoped with this key
include Dry::Effects::Handler.Cache(:blog)

Rubyアプリケーション用のファイルアップロードツールキットを提供する優れたShrinegemでも、同様の使用法を見つけることができます。

class Photo < Sequel::Model
  include Shrine::Attachment(:image)
end

このパターンはまだ比較的新しいものですが、特にRailsよりも純粋なRubyアプリケーションに重点を置いているgemで、今後さらに多くのパターンが見られると思います。

概要

この投稿では、Rubyで構成可能なモジュールを実装する方法について説明しました。これは、モジュールビルダーパターンと呼ばれることもある手法です。他のメタプログラミング手法と同様に、これには複雑さが増すという犠牲が伴います。したがって、正当な理由がない限り使用しないでください。ただし、このような柔軟性が必要となるまれなケースでは、Rubyのオブジェクトモデルにより、エレガントで簡潔なソリューションが再び可能になります。モジュールビルダーパターンは、ほとんどのRuby開発者が頻繁に必要とするものではありませんが、特にライブラリ作成者にとって、ツールキットに含めるのに最適なツールです。

P.S。 Ruby Magicの投稿をマスコミから離れたらすぐに読みたい場合は、Ruby Magicニュースレターを購読して、投稿を1つも見逃さないでください。


  1. Rubyのデコレータデザインパターン

    デコレータのデザインパターンは何ですか? そして、Rubyプロジェクトでこのパターンをどのように使用できますか? デコレータデザインパターンは、新機能を追加することでオブジェクトを強化するのに役立ちます クラスを変更せずにそれに。 例を見てみましょう! ロギングとパフォーマンス この例では、rest-clientのようなgemを使用してHTTPリクエストを作成しています。 次のようになります: require restclient data = RestClient.get(www.rubyguides.com) 今 : 一部のリクエストにログを追加したいが、RestCli

  2. Ruby Enumerable Moduleの基本ガイド(+私のお気に入りの方法)

    列挙可能とは何ですか? 列挙可能は反復法のコレクションです 、Rubyモジュール、そしてRubyを優れたプログラミング言語にする大きな部分です。 列挙可能には次のような便利なメソッドが含まれます : マップ 選択 注入 列挙可能なメソッドは、ブロックを与えることで機能します。 そのブロックで、すべての要素で何をしたいのかを伝えます。 例 : [1,2,3].map { |n| n * 2 } すべての数値が2倍になった新しい配列を提供します。 正確に何が起こるかは、使用する方法、 mapによって異なります。 すべての値を変換するのに役立ちます、 select リス