構成可能な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
という単純なモジュールを作成します。 、次の動作を実装します:
-
Wrapper
を含むクラス 特定のタイプのオブジェクトのみをラップできます。コンストラクターは引数の型を検証し、型が期待されるものと一致しない場合はエラーを発生させます。 - ラップされたオブジェクトは、
original_<class>
というインスタンスメソッドを介して利用できます。 、例:original_integer
またはoriginal_string
。 - これにより、コードのコンシューマーは、このアクセサーメソッドの代替名(
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つも見逃さないでください。
-
Rubyのデコレータデザインパターン
デコレータのデザインパターンは何ですか? そして、Rubyプロジェクトでこのパターンをどのように使用できますか? デコレータデザインパターンは、新機能を追加することでオブジェクトを強化するのに役立ちます クラスを変更せずにそれに。 例を見てみましょう! ロギングとパフォーマンス この例では、rest-clientのようなgemを使用してHTTPリクエストを作成しています。 次のようになります: require restclient data = RestClient.get(www.rubyguides.com) 今 : 一部のリクエストにログを追加したいが、RestCli
-
Ruby Enumerable Moduleの基本ガイド(+私のお気に入りの方法)
列挙可能とは何ですか? 列挙可能は反復法のコレクションです 、Rubyモジュール、そしてRubyを優れたプログラミング言語にする大きな部分です。 列挙可能には次のような便利なメソッドが含まれます : マップ 選択 注入 列挙可能なメソッドは、ブロックを与えることで機能します。 そのブロックで、すべての要素で何をしたいのかを伝えます。 例 : [1,2,3].map { |n| n * 2 } すべての数値が2倍になった新しい配列を提供します。 正確に何が起こるかは、使用する方法、 mapによって異なります。 すべての値を変換するのに役立ちます、 select リス