こんにちは、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, @y=5>
モジュールには他にも多くの側面がありますが、ここまではごく一部を紹介するにとどめました。しかし本記事の主眼はModule Builderのビルダーの方であり、モジュールそのものではないので、今度はビルダーを見てみることにしましょう。
モジュールをビルドするモジュール
モジュールが強力である理由は、機能の特定の共有パターンをカプセル化するからです(効果的に使えばの話ですが)。先の例で言うと、この機能は、変数のペア(座標の点や、x
とy
をペアで持つ任意の他のクラス)を足すことです。このような共有パターンを見い出してモジュール(またはモジュールのセット)の形で書くことで、複雑な問題をシンプルかつモジュラリティの高いソリューションによって扱えるようになります。
しかし先のモジュールには「変数x
とy
を備えたクラス」という制限がまだ残っているため、一般性はそれほど高くありません。この制限を外して、任意の変数セットで使えるようにできるとしたらどうでしょう。しかしどうやって?
なお一度定義されたモジュールは設定できなくなるので、制限を外すには他の方法が必要です。1つの方法は、「必要な機能を自分自身がクラスに追加する別のモジュール」をモジュール上に定義することです。このメソッドを#define_adder
と呼ぶことにします5。
module AdderDefiner
def define_adder(*keys)
define_method :+ do |other|
self.class.new(*(keys.map { |key| send(key) + other.send(key) }))
end
end
end
ここでは、Addable
モジュールでやったような#+
メソッドの定義ではなく、メソッドを定義するメソッドを定義します。このメソッドはモジュールのインスタンスメソッドを動的にクラスに文字どおり「ブートストラップ」する(=自分で自分を持ち上げる)ので、本記事では以後、メソッドを定義するメソッドを「ブートストラップメソッド」と呼ぶことにします。
このブートストラップメソッドは任意の数の引数を取り、splat演算子*
で配列keys
に割り当てます。配列keys
は、足し算する変数の名前(厳密には、変数の値を返すメソッドの名前)になります。次に#+
というメソッドを定義します。このメソッドはキーごとに変数の値を足し、その結果を使ってself.class.new
でインスタンスをひとつ作成します。
この方法でうまくいくかどうかやってみましょう。いくつかの異なるreader属性を持つ新しいクラスLineItem
を作成します。#define_adder
をクラスメソッドにする必要があるため、モジュールを(include
ではなく)extend
します。
class LineItem < Struct.new(:amount, :tax)
extend AdderDefiner
define_adder(: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>
動きました!しかも、変数名にも変数の個数にも依存していません。つまり、モジュールの柔軟性がさらに高まったのです。
しかしここでひとつ問題があります。先のログ出力コードをPoint
クラスからコピーし、次のように#+
に貼り付けてログ出力を呼び出すことにします。
class LineItem < Struct.new(:amount, :tax)
extend AdderDefiner
define_adder(:amount, :tax)
def +(other)
puts "Enter Adder..."
super.tap { puts "Exit Adder..." }
end
end
ここで再び2つの行項目を足してみると、ログは出力されず、例外が発生してしまいます。
NoMethodError: super: no superclass method `+' for #<struct LineItem amount=9.99, tax=1.5>
何が起こったのでしょうか?
#define_adder
が#+
メソッドをどのように「ブートストラップ」しているのか、よく見てみましょう。#define_adder
は#define_method
でメソッドをクラス(ここではLineItem
)に直接追加しています。クラスは、渡されたメソッドの定義を1つしか持てないため、クラス内で後から#+
を定義すると元の定義が飛んでしまいます。メソッドが定義されるときにはinclude
も継承も行われていないため、ここには「スーパークラス」はありません。
この問題を解決するには「モジュールをビルドする」というマジックが少々必要です。最初に問題の解決方法を紹介し、次にこの問題をどのように解決しているかを解説します。
module AdderIncluder
def define_adder(*keys)
adder = Module.new do
define_method :+ do |other|
self.class.new(*(keys.map { |key| send(key) + other.send(key) }))
end
end
include adder
end
end
先ほどのLineItem
を使ったログ出力コードで、AdderDefiner
モジュールの代わりにこのAdderIncluder
モジュールを使うと、今度は次のとおり正常に動作します。
l1 = LineItem.new(9.99, 1.50)
l2 = LineItem.new(15.99, 2.40)
l1 + l2
Enter Adder...
Exit Adder...
#=> #<LineItem:... @amount=25.98, @tax=3.9>
コードが動作する秘密は、#define_adder
での#+
メソッド定義で無名モジュールを使っていることです。これは先ほどのような、呼び出し側のクラスでメソッドを直接定義する手法とは異なります。
Rubyのあらゆるモジュール(これは小文字のmodule、つまりRubyの一般的なモジュールである点にご注意ください)は、あるクラスの単なるインスタンスであることを思い出しましょう。そして「あるクラス」とは、大文字のModule
クラスなのです。Module
クラスは他のクラスと同様にnew
メソッドを持つので、ここではブロック引数をひとつ取り、そのブロックは新しく作成されたモジュールのコンテキストで評価されます。
モジュールをその場で生成したければ、単にModule.new
にメソッド定義を含むブロックを渡せばよいのです。このようにして生成したモジュールは、通常の(名前付き)モジュールと同じように利用できます。
mod = Module.new do
def with_foo
self + " with Foo"
end
end
title = "The Ruby Module Builder Pattern"
title.extend mod
title.with_foo
#=> "The Ruby Module Builder Pattern with Foo"
これはテストでよく使われる手軽なトリックで、1度きりのテストでしか使わない使い捨てモジュールのためにグローバル名前空間を汚したくない場合に使います(Class
にも使えます)6。
AdderIncluder
モジュール内のブロックで定義されているメソッドは、AdderDefiner
で定義した#+
メソッドと同じであり、その意味で2つのモジュールの動作はきわめて似通っています。両者の重要な違いは、AdderDefiner
の場合はメソッドをクラス上に定義しているのに対し、AdderIncluder
の場合はメソッドを含むモジュールをクラスにinclude
している点です。
次のようにancestors
でチェインを表示すれば、このモジュールがLineItem
クラス自身の直下にあることを確認できます。
LineItem.ancestors
#=> [LineItem,
#<Module:0x...>,
...
このancestors
チェインを見れば、ここでsuper
が正常に動作する理由がはっきりわかります。LineItem
の#+
を呼ぶとこのメソッドからsuper
が呼ばれてクラス階層を遡り、ここで使いたい#+
が定義されている無名モジュールにたどりつきます。続いて#+
で足し算が実行されて結果を返します。この結果は、LineItem
の足し算でログ出力コードをコンポジションにしたものです。
この動作は一見奇妙かつ見慣れないものですが、ブートストラップメソッドで無名モジュールを動的にinclude
するテクニックは実際非常に便利であり、こうした理由によって広く使われています。特にRailsのように、モデルを定義したりルーティングを設定するだけでdefine_adder
的なブートストラップメソッド呼び出しが多数トリガされる柔軟なフレームワークで多用されています。このテクニックは、これと同じレベルの柔軟性を必要とするその他のgem(私のMobilityなど)でも使われています。
このように、モジュールビルドのブートストラップはRubyにおいてきわめて重要なメタプログラミング技法であると考えられます。実際、このテクニックはRailsのようなフレームワークのきわめて動的な特性を支えています。
しかしここからが本題です。私としてはこのブートストラップ技法よりも、これからご紹介するModule Builderの方がはるかに高い能力を示すことを皆さまに理解いただきたいと考えており、その意味でModule Builderパターンはブートストラップよりもいっそう注目に値します。説明のため、先のAdder
モジュールに立ち返り、先ほどとは少し違う方法でこのモジュールを定義することにします。
(「#2 Module Builderパターンとは何か」に続く)
図版(#1で使用されているもの)
a. “Drawing Hands” by M.C. Escher. reference
b. “Automated space exploration and industrialization using self-replicating systems.” From Advanced Automation for Space Missions: 5. REPLICATING SYSTEMS CONCEPTS: SELF-REPLICATING LUNAR FACTORY AND DEMONSTRATION. reference
関連記事
-
私がこの「
Module
クラスのサブクラス化」というアイデアを最初に知ったのはPiotr Solnicaによる2012年の記事でした。最近だとEric AndersonやAndreas Robeckeの記事でも言及されていますが、それ以外にはほとんど見かけません。 ↩ - 記事執筆時点では、こことここで使われています。 ↩
-
ここでは、モジュールを別の場所で
include
し、メソッドチェインの複雑なコンポジションをsuper
で形成するという意味です。このテクニックはMobilityで多用しました。 ↩ - 正確に言うと、2つの属性の値を引数として取るイニシャライザも必要です。 ↩
-
ここでは、
include
でモジュールのメソッドをクラスメソッドとしてクラスに追加するというトリックを使っています。このクラスはモジュールがinclude
されるときにextend
されます。これはよくあるトリックですが、これがちょっと謎に見えるのであれば、先に進む前にこの謎を読み解いてみてください。 ↩ - 無名モジュールのこのような「名前空間を汚さない」性質はメタプログラミングでも有用です(定数名の衝突を気にしないで済むため)。 ↩