サーバサイドエンジニアの中村です。先日開催された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に参加するまでは知りませんでした。他のリポジトリのコードからも学びつつ、社内で良い設計とは何かについてまたさらに議論したいと思います。
またありがたいことに、弊社では国内・海外のカンファレンス参加は出張扱いになっており、カンファレンス参加費・交通費・宿泊費(一部)は会社負担になります。業務として色んなカンファレンスに参加したい!という方はぜひご応募お待ちしています。