トレタ開発者ブログ

飲食店向け予約/顧客台帳サービス「トレタ」、モバイルオーダー「トレタO/X」などを運営するトレタの開発メンバーによるブログです。

RubyKaigi2017とModule Builder Pattern #rubykaigi

サーバサイドエンジニアの中村です。先日開催されたRubyKaigi2017に参加しました。 その中で最も興味深かったセッションである The Ruby Module Builder Pattern について自分なりの理解をまとめてみようと思います。

Moduleの問題

module内でdefine_methodを使ってメソッド定義を行ったmethodに対して、そのmoduleをextendしたclass内で該当のメソッドに対して super を呼び出すことができません。

module AdderDefiner
  def define_adder(*keys)
    define_method :+ do |other|
      self.class.new(
        *(keys.map { |key| send(key) + other.send(key) })
      )
    end
  end
end

class LineItem < Struct.new(:amount, :tax)
  extend AdderDefiner

  define_adder(:amount, :tax)

  def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." } #=> No Method Error
  end
end

これは、+ methodが define_methodを使ってmodule内 で定義されているため、super で呼び出す祖先Object内で+ methodが定義されていないためだと認識しています。

これを回避するために、Anonymous Moduleを使ってこの祖先Object内で定義済みにするテクニックを今回のセッションで初めて知りました。

module AdderDefiner
  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 # この時点でAnonymous Moduleをincludeすることでsuperが利用可能
  end
end

このテクニックを応用したパターンがModule Builder Patternです。

The Ruby Module Builder Pattern

発表者の @shioyama さんに Module Builder Pattern を理解するための良いサンプルコードはありますかと質問したところ、 http://dry-rb.org/dry-equalizer というgemのコードがオススメだと教えていただきました。

そこで、本記事では dry-equalizer のコードを例にModule Builder Patternを理解していきたいと思います。

dry-equalizer

dry-equalizer gemはincludeしたclassの以下4つのmethodを再定義します。

Object#inspect
Object#hash
Object#eql?
Object#==

具体的な挙動は Examples のようになります。この挙動を踏まえて実際のコードを見ていきましょう。

Module Builder Patternのポイントは、Module.class #=> Class であることを利用して、動的にModuleを生成している部分です。

class Equalizer < Module

このように、Equalizer classを Module のsubclassとして定義することで、Anonymous Moduleのメリットが利用できます。

次に、included を見ていきます。

def included(descendant)
    super
    descendant.send(:include, Methods)
end

Equalizer が他classにincludeされた際に、

def eql?(other)
    instance_of?(other.class) && cmp?(__method__, other)
end

def ==(other)
   other.is_a?(self.class) && cmp?(__method__, other)
end

をincludeして再定義します。

そして最後に initializeです。

def initialize(*keys)
  @keys = keys
  define_methods
  freeze
end

初期化するkeyを受取り、define_methods を実行し、cmp?, hash, inspectを定義します。

このように、Module Builder Patternを使うと、非常にわかりやすくメソッドを動的に生成することができます。

おわりに

私はAPIのコードを書く上で常に設計についてどうするのがより良いか、チームの人と議論することが多く、今回のModule Builder Patternのような話は良い設計の1つのテーマとして非常に良いお話でした。

http://dry-rb.org というRubyの素晴らしい設計を集めたリポジトリも今回のRubyKaigiに参加するまでは知りませんでした。他のリポジトリのコードからも学びつつ、社内で良い設計とは何かについてまたさらに議論したいと思います。

またありがたいことに、弊社では国内・海外のカンファレンス参加は出張扱いになっており、カンファレンス参加費・交通費・宿泊費(一部)は会社負担になります。業務として色んなカンファレンスに参加したい!という方はぜひご応募お待ちしています。

© Toreta, Inc.

Powered by Hatena Blog