RubyのModule Builderパターン #1 モジュールはどのように使われてきたか(翻訳)

こんにちは、hachi8833です。今回の翻訳記事は、Rubyならではのデザインパターンとでも言うべき「Module Builderパターン」の詳細な解説です。RubyのModuleが実はクラスであることをうまく利用していて、Railsなどのフレームワーク側で有用性が高いパターンであるように思えました。 元記事が非常に長いので次のように分割しました。 #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さんことChris Salzbergさんの記事です。このときのセッションでもModule Builderパターンを扱っています。 参考: RubyKaigi 1日目セッション: The Ruby Module Builder Pattern 本記事の図はすべて元記事からの引用です。また、パターン名は英語で表記しました。 RubyのModule Builderパターン #1 モジュールはどのように使われてきたか(翻訳) 最近私はRubyできわめてパワフルなパターンを発見しましたが、今のところあまり知られておらず、その価値もそれほど理解されていないようです1。私はこれにModule Builderパターンと名付け、Mobility gemの設計で多用しています。Mobilityは数か月前に私がリリースした、プラグイン機能を持つ翻訳/多言語フレームワークであり、自分でも非常に重宝しています。そして私は、Mobility開発中に学んだことを皆さんにも共有すべきと感じたのです。 このパターンの中核にあるModule Builderはきわめてシンプルかつエレガントであり、クラスにmixinされるカスタマイズ可能な名前付きモジュールを動的に定義できるという万能性を備えています。これを実現しているのはModuleクラスのサブクラス化であり、これらサブクラスは実行時に初期化され、そこで生成されたモジュールが他のクラスにincludeされます。 「Moduleクラスのサブクラス化」と聞いて頭がクラクラしてくるようでしたら、ぜひこの先もお読みください。Module Builderは一見深遠な奥義のように見えて、その実非常に有用な側面があります。Module Builderは、dry-rbなどのプロジェクトで大規模に利用されて大きな成果を上げているのですが、Module Builderのせっかくの本質的なシンプルさは高度なコーディングスタイルに覆い隠されてしまい、見えなくなってしまっています。Module BuilderパターンはRailsでもときおり使われています2が、ごく小規模にとどまっています。Rubyistの多くはこのパターンに気づいていないと言ってしまってよいと思います(日常の業務で使ったことが一度でもあるかどうかは別にしても)。 Module Builderについて解説するために、革新的なまでに複雑なコード例を多数ご紹介し、最後にMobilityからMethodFoundというgemに切り出されたコードの中から、私の書いたコードをいくつかご紹介します。これらの例は、まず多くの読者が理解できる中心となるシンプルなコンセプトから始まり、続いて少しばかり高度な話題に進み、現実のアプリがこのパターンから明らかに大きなメリットを得られることを示します。 Rubyのモジュールについて 最初にRubyの「モジュール」についておさらいしておきましょう。モジュールは基本的にメソッドや定数の集まりであり、どのクラスにもincludeでき、コードの重複を軽減し、再利用やコンポジション3を行いやすい単位で機能をカプセル化します。includedやextendedといったコールバックを使えば、モジュールがクラスやモジュールにinclude(またはextendなど)されるたびに何らかのカスタムコードを実行できます。 さて、以下のMyModuleというモジュールに#fooというメソッドがあるとしましょう。 module MyModule def foo “foo” end end これで、MyModuleをincludeしたクラスに#fooメソッドを追加できます。 class MyClass include MyModule end a = MyClass.new a.foo #=> “foo” これはかなり正統な使い方ですが、大きな限界があります。#fooは事前に定義されているので変えられず、戻り値もハードコードされています。 次にもう少しだけ「現実的な」場合を見てみましょう。Pointというクラスに2つの属性xとyがあるとしましょう。話を簡単にするためにStructで定義します。 Point = Struct.new(:x, :y) それでは、2つの点のxの値とyの値をそれぞれ足し算できるモジュールをひとつ作成しましょう。このモジュールをAddableと呼ぶことにします。 module Addable def +(point) Point.new(point.x + x, point.y + y) end end Point.include Addable p1 = Point.new(1, 2) p2 = Point.new(2, 3) p1 + p2 #=> #<Point:.. @x=3, @y=5> 今度は先ほどのモジュールよりも再利用性がやや高まりましたが、それでも頑固な制限が残っています。このモジュールは属性xとyを持つPointというクラスがないと機能しないのです。 この定数Pointをモジュールから削除し、self.classに置き換えることでこの点を少し改善できます。 module Addable def +(other) self.class.new(x + other.x, y + other.y) end end これでAddableモジュールは、アクセサxとyがある任意のクラス4で利用できるようになり、柔軟性がかなり高まりました。さらにRubyはきわめて動的に型を扱えるので、これらの要素に#+メソッドがありさえすれば、(潜在的には)このモジュールを使って2つのデータ型のさまざまな組み合わせを足し算できるようになります。 しかもRubyのモジュールは、superキーワードによる継承やincludeを使ってモジュールのメソッドをコンポジションにできる点にもご注目ください。したがって、この足し算メソッド呼び出しでログを出力したければ、次のようにPointクラスの内部で#+を定義すればよいのです。 class Point < Struct.new(:x, :y) include Addable def +(other) puts “Enter Adder…” super.tap { puts “Exit Adder…” } end end 期待どおり、次のようにログが出力されます。クラスのメソッドが最初に呼び出され、モジュール内に定義されたメソッドでsuperされます。 p1 = Point.new(1, 2) p2 = Point.new(2, 3) p1 + p2 Enter Adder… Exit Adder… #=> #<Point:.. @x=3, … Continue reading RubyのModule Builderパターン #1 モジュールはどのように使われてきたか(翻訳)