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

こんにちは、hachi8833です。今回の翻訳記事は、Rubyならではのデザインパターンとでも言うべき「Module Builderパターン」の詳細な解説です。RubyのModuleが実はクラスであることをうまく利用していて、Railsなどのフレームワーク側で有用性が高いパターンであるように思えました。

元記事が非常に長いので次のように分割しました。

追記(2017/10/27)

  • 元記事からTechRachoにリンクいただきました🙇

概要

原著者の許諾を得て翻訳・公開いたします。

RubyKaigi 2017@広島でも発表されたshioyamaさんことChris Salzbergさんの記事です。このときのセッションでもModule Builderパターンを扱っています。

本記事の図はすべて元記事からの引用です。また、パターン名は英語で表記しました。

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を行いやすい単位で機能をカプセル化します。includedextendedといったコールバックを使えば、モジュールがクラスやモジュールにinclude(またはextendなど)されるたびに何らかのカスタムコードを実行できます。

さて、以下のMyModuleというモジュールに#fooというメソッドがあるとしましょう。

module MyModule
  def foo
    "foo"
  end
end

これで、MyModuleincludeしたクラスに#fooメソッドを追加できます。

class MyClass
  include MyModule
end

a = MyClass.new
a.foo
#=> "foo"

これはかなり正統な使い方ですが、大きな限界があります。#fooは事前に定義されているので変えられず、戻り値もハードコードされています。

次にもう少しだけ「現実的な」場合を見てみましょう。Pointというクラスに2つの属性xyがあるとしましょう。話を簡単にするために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>

今度は先ほどのモジュールよりも再利用性がやや高まりましたが、それでも頑固な制限が残っています。このモジュールは属性xyを持つPointというクラスがないと機能しないのです。

この定数Pointをモジュールから削除し、self.classに置き換えることでこの点を少し改善できます。

module Addable
  def +(other)
    self.class.new(x + other.x, y + other.y)
  end
end

これでAddableモジュールは、アクセサxyがある任意のクラス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のビルダーの方であり、モジュールそのものではないので、今度はビルダーを見てみることにしましょう。

「モジュールをビルドするモジュール」をビルドする

「モジュールをビルドするモジュール」をビルドする

モジュールをビルドするモジュール

モジュールが強力である理由は、機能の特定の共有パターンをカプセル化するからです(効果的に使えばの話ですが)。先の例で言うと、この機能は、変数のペア(座標の点や、xyをペアで持つ任意の他のクラス)を足すことです。このような共有パターンを見い出してモジュール(またはモジュールのセット)の形で書くことで、複雑な問題をシンプルかつモジュラリティの高いソリューションによって扱えるようになります。

しかし先のモジュールには「変数xyを備えたクラス」という制限がまだ残っているため、一般性はそれほど高くありません。この制限を外して、任意の変数セットで使えるようにできるとしたらどうでしょう。しかしどうやって?

なお一度定義されたモジュールは設定できなくなるので、制限を外すには他の方法が必要です。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

関連記事

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

[保存版]人間が読んで理解できるデザインパターン解説#1: 作成系(翻訳)

[保存版]人間が読んで理解できるデザインパターン解説#2: 構造系(翻訳)

Rubyで学ぶデザインパターンを社内勉強会で絶賛?連載中


  1. 私がこの「Moduleクラスのサブクラス化」というアイデアを最初に知ったのはPiotr Solnicaによる2012年の記事でした。最近だとEric AndersonAndreas Robeckeの記事でも言及されていますが、それ以外にはほとんど見かけません。 
  2. 記事執筆時点では、ここここで使われています。 
  3. ここでは、モジュールを別の場所でincludeし、メソッドチェインの複雑なコンポジションをsuperで形成するという意味です。このテクニックはMobilityで多用しました。 
  4. 正確に言うと、2つの属性の値を引数として取るイニシャライザも必要です。 
  5. ここでは、includeでモジュールのメソッドをクラスメソッドとしてクラスに追加するというトリックを使っています。このクラスはモジュールがincludeされるときにextendされます。これはよくあるトリックですが、これがちょっと謎に見えるのであれば、先に進む前にこの謎を読み解いてみてください。 
  6. 無名モジュールのこのような「名前空間を汚さない」性質はメタプログラミングでも有用です(定数名の衝突を気にしないで済むため)。 
Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833

コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。
これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。
かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。
実は最近Go言語が好き。
仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

BPSアドベントカレンダー

週刊Railsウォッチ

インフラ

BigBinary記事より

ActiveSupport探訪シリーズ