RubyのModule Builderパターン #2 Module Builderパターンとは何か(翻訳)

元記事が非常に長いので次のように分割しました。 #1 モジュールはどのように使われてきたか #2 Module Builderパターンとは何か(本記事) #3 Rails ActiveModelでの利用例 あとがき: Module Builderパターンという名前について 追記(2017/10/27) 元記事からTechRachoにリンクいただきました🙇 shioyamaさんがRailsに投げたプルリクを知らせていただきました: #30895 Convert AttributeMethodMatcher to Module Builder 概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: The Ruby Module Builder Pattern 公開日: 2017/05/20 著者: Chris Salzberg サイト: dejimata.com RubyKaigi 2017@広島でも発表されたshioyamaさんです。このときのセッションでもModule Builderパターンを扱っています。 RubyKaigi 1日目セッション: The Ruby Module Builder Pattern 本記事の図はすべて元記事からの引用です。また、パターン名は英語で表記しました。 RubyのModule Builderパターン #2 Module Builderパターンとは何か(翻訳) Module Builder ここまでに扱った3種類のadder系モジュールを振り返ってみましょう。 Addable: 2つの変数xとyの#+メソッドを追加する。 AdderDefiner: クラスメソッドdefine_adder(任意の変数のセットに#+メソッドを定義する)を追加する。 AdderIncluder: これもクラスメソッドdefine_adder(#+メソッドを定義する)を追加するが、このメソッドはsuperでコンポジションにできる。 1.のAddableモジュールはさほど柔軟ではありませんが、今思えば、これはこれでいくつかよい点があります。 #+だけをincludeするので、define_adderのような人工的なブートストラップが残らない(AdderDefinerやAdderIncluderはそうではない) superで親クラスからメソッドにアクセスできる(AdderIncluderでもできるがAdderDefinerではできない) モジュールに名前があるのでancestorsチェインで簡単に確認できる。AdderDefinerやAdderIncluderの場合、それ自身はancestorsチェインに表示されるが、ブートストラップメソッドが呼び出されたかどうか、どのように呼び出されたかの確認が困難(または確認不能)。 このセクションでは、こうしたadder系モジュールの利点を保つ方向で問題を解決する方法をご紹介します。前のセクションで追求した柔軟性もこの方法で得られます。 最初に、Moduleクラスを継承するAdderBuilderクラスを定義します。 class AdderBuilder < Module def initialize(*keys) define_method :+ do |other| self.class.new(*(keys.map { |key| send(key) + other.send(key) })) end end end 小さなクラスですが、とても密度の高い内容です。このクラスで行われていることを詳しく説明する前に、動作を確認してみましょう。ここでは、このクラスをインスタンス化してモジュールにしたものをLineItemに(extendではなく)includeします。 class LineItem < Struct.new(:amount, :tax) include AdderBuilder.new(:amount, :tax) end l1 = LineItem.new(9.99, 1.50) l2 = LineItem.new(15.99, 2.40) l1 + l2 #=> #<LineItem:… @amount=25.98, @tax=3.9> すると、このちっぽけなクラスはAdderDefiner内のブートストラップメソッド#define_adderと同じように動作します。唯一違う点は、この動作のためのクラスメソッドをまったく定義していないことです。 いったいどうやっているのでしょうか?まず、ここではModuleクラスをサブクラス化しています。Moduleクラスはnewを呼べるので、サブクラス化してinitializeなどのメソッドを独自に定義できるだろうと推測できます。そして先に示したとおり、本当にできるのです。 クラスの初期化部分を再録します。 def initialize(*keys) define_method :+ do |other| self.class.new(*(keys.map { |key| send(key) + other.send(key) })) end end このコードには2つのレベル(訳注: ネストのこと)があり、両方のレベルを認識することが重要です。1つのレベルでは、Moduleクラスの新しいインスタンスを初期化しています。このインスタンスはそれ自身がいわゆる(小文字の)モジュールであり、他のクラスにmixinできるメソッドコレクションです。次のレベルではこのモジュールを初期化し、メソッドのひとつとして#+を定義しています。 この#+メソッド自身は、Module.newに渡される引数(keysは上述の:amountと:taxです)に含まれるメソッド名に沿って定義されます1。このメソッドの中でself.class.newを呼ぶと、この新しいモジュールをincludeするクラスのコンテキストでこのnewが評価され、そこでLineItem(またはPointのようにこのモジュールをincludeしたクラスなら何でもよい)を評価します。 すなわちAdderBuilder.new(:amount, :tax)は、以下と機能的に同等な方法でモジュールを評価します。 Module.new do def +(other) self.class.new(amount + other.amount, tax + other.tax) end end 上はAddableで使ったコードですが、xとyはそれぞれamountとtaxに差し替えられます。しかしこのパワーは、モジュールの動的な定義で任意の変数名を指定できるという事実から生み出されています。 そして、このパターンの真の創意はまさしくここに潜んでいるのです。すなわち、モジュールを「メソッドや定数が固定されたコレクション」としてではなく、「欲しいコレクションをその場でビルドできる設定可能なプロトタイプ」として定義できるということです。これならクラスメソッドも不要ですし、モジュールのインクルード手順をブートストラップするメタプログラミング的トリックも不要です。このビルダはモジュールを直接ビルドするので、1ステップでモジュールをインクルードできます。 それだけではありません。先のセクションで解説したテクニックで生成される無名モジュールと異なり、この方法で作成されるモジュールには名前があるので、次のようにancestorsチェインではっきりと確認できます。 LineItem.ancestors [LineItem, #<AdderBuilder:0x…>, Object, … ] これはまさしく、Moduleクラスのサブクラス化で達成したカプセル化の素敵な副産物です2。無名モジュールでは、以下のようにデバッグ時に読み取りにくくなります。 LineItem.ancestors [LineItem, #<Module:0x…>, Object, … ] Module Builderパターンはこのように、無名モジュールのメリット(モジュールのインスタンスを「その場で」定義でき、かつグローバル定数の名前空間を汚さない)を得ながら、通常のモジュールと同様にインスタンスが名前を持つので、デバッグトレースが簡単です。そのうえ、ブートストラップするクラスをincludeするクラスでは新しいメソッドを定義する必要も呼び出す必要もありません。 それだけではありません。先ほどのコードでは、includeする側のクラスにログ出力のコードを少々追加しましたが、これもModule Builderとして書き直せます。これをLoggerBuilderと呼ぶことにします。 class LoggerBuilder < Module def initialize(method_name, start_message: “”, end_message: “”) define_method method_name do … Continue reading RubyのModule Builderパターン #2 Module Builderパターンとは何か(翻訳)