Module Builder Patternと出会う

RubyKaigi2017帰りの@mactkgです。これからちまちまとブログを書いていくと思いますが、よろしくお願いします。普段はRubyでRailsを書いています。

入社してから毎日Ruby(たまにiOSとSQL)を書いている日々ですが、使用しているgemのコードを眺める中でModuleの表現力に驚かされることが多々ありました。そんな中参加したRubyKaigi2017での個人的テーマは、Moduleだったのではないかと振り返って感じます(MatzのKeynoteも、Moduleの話でした)。

そんな中会場で聞いた「The Ruby Module Builder Pattern」というトークが面白かったのですが、最近使ったShrineという画像アップロードのgemの実装とつながり、ビビっと来ましたので記事にします。まずはModule Builder Patternから…。

Module Builder Patternが解決する問題

講演からコードを引用してModule Builder Patternが解決する問題を紹介します。Module同士の足し算の機能を追加する AddableというModuleを考えます。

# 足し算の機能を追加するAddable Module
module Addable
  def +(other)
     self.class.new(x + other.x, y + other.y)
  end
end

# xとyを持つPoint StructにAddableをincludeして
# 足し算の機能を追加する
Point = Struct.new(:x, :y) do
  include Addable

  # オーバーライドして定義もできる
  def +(other)
    puts ":+ called"
    super(other)
  end
end

# 使用例
p Point.new(42, 4) + Point.new(10, 1) 
#=> :+ called
#=> #<struct Point x=52, y=5>

このModuleはxyを持ったオブジェクトにのみ適用可能で、定義が固定になっています。xyzを足すようにしたかったり、pricetaxを足すようにしたい場合には流用できません1)講演の中では、ここから何ステップか踏んで、Module Builder Patternの紹介に入ります。ぜひスライドも合わせてご覧になってください。

# Attributeの名前が異なるStructにincludeすると…
ItemPrice = Struct.new(:price, :tax) do
  include Addable
end

# xとyが無いのでNameErrorになる
p ItemPrice.new(100, 8) + ItemPrice.new(200, 216)
#=> `+': undefined local variable or method `x' for #<struct ItemPrice price=100, tax=8> (NameError)

Module Builder Pattern

そこでModule Builder Patternを導入してみます。Module はClassである(Module.class #=> Class)ことを利用して、Moduleを動的に定義するModule Builderを作ります。具体的には、Moduleを継承したクラスを作ることで実装します。コード例を次に示します。

# module.class #=> Class なので、Moduleは継承可能。
class AdderBuilder < Module
  # インスタンス生成時に+メソッドを定義する
  def initialize(*keys)
    # インスタンス生成時に渡されたkeysを元に、足し合わせていく
    define_method :+ do |other|
      result = keys.map { |key| send(key) + other.send(key) }
      self.class.new(*result)
    end
  end
end

これだけです。使うときはインスタンス化して使います。

# Module Builderを使うときは、initializeしてincludeする
Point3d = Struct.new(:x, :y, :z) do
  include AdderBuilder.new(:x, :y, :z)
end

a = Point3d.new(42, 4, 10)
b = Point3d.new(10, 1, 100)

p a + b #=> #<struct Point3d x=52, y=5, z=110>

この作成方法の面白いところは、様々な用途でModuleを使いまわせることではないかと感じています。例えば緯度・経度を表す場合でもそのまま使うことができます。

City = Struct.new(:latitude, :longitude) do
  include AdderBuilder.new(:latitude, :longitude)
end

tokyo = City.new(35.652832, 139.839478)
rio = City.new(-22.970722, -43.182365)

p tokyo + rio #=> #<struct City latitude=12.682109999999998, longitude=96.65711300000001>

ところで今回は足し算を扱ってみたのですが、この場合だとModuleがinitializeに必要な引数などを知っておく必要があります。例えばCityのinitializeに都市名を扱うnameという引数が先頭に追加された場合に、意図通りに引数が渡らないという問題が起こってしまいます。この問題については解決策が見えていません…。

Module Designの三要素

Module Builder Patternを紹介しましたが、講演の中で、@shioyamaさんはModule Designについて3つの要素を挙げていました。

  • 抽象化: 共通するパターンを1つの機能の単位として抽出する
  • 仕様化: その単位をインターフェースに落とし込む
  • カプセル化: 実装をPublicのインターフェースから隠蔽する

Module Builder Patternは、これら3つの要素をうまくまとめ上げたパターンになっていると思います。まずは最初の例のように、具体的なユースケースで落とし込んでModuleを作り、そこから抽象化をしてインタフェースに落とし込み、define_method などを使いながらModule Builderを仕上げていくと良いのかなと考えています。

Module Builder in Shrine

最後に、私がRubyKaigi前に観測したModule Builder Patternを紹介します。

最近Railsでファイルアップロードを行うのに、Shrineというgemを使っています。Shrineを使うのに少しgemの中身を読んでいたところ、Module Builder Patternと出会い、RubyKaigiでのトークに話がつながりました。

次のコードはShrineを使い、Userモジュールにavatarという名前の画像を持たせる場合のコードです。

class User < ApplicationRecord
  # Module Builder Pattern!!!!!!
  include ImageUploader::Attachment.new(:avatar)
end

ImageUploaderはユーザーが定義したClassで、アップロード時の作業などをユーザーが定義したものです。ImageUploaderShrineを継承しており、Shrine::AttachmentModuleを継承したClassです。

class ImageUploader < Shrine
  plugin :activerecord
  plugin :pretty_location
end
class Shrine
  class Attachment < Module
  end
  
  #... AttachmentのClassMethodやInstanceMethodが定義されている
  #... Plugin機構にするために、少し複雑!(面白いのでぜひ読んでみてください)
end

すなわち、ImageUploader::Attachment.superclassModuleです!(感動するところです)

おわりに

今回は、Module Builder Patternについてフォーカスして記事を書きました。少し長くなってしまいましたが、読みながら実際に実行してみることで、理解が深められるかと思います。

Module Builder Patternは、gemを開発するのに便利そうだなと感じています。Moduleを継承するだけのシンプルなパターンですが、覚えておくと表現の幅がグッと広がります。他にModule Builder Patternを使ったgemとしては、dry-equalizerがあります。(@shioyamaさんのブログ記事より) 私も読んでみたのですが、シンプルで読みやすいです。

来年のRubyKaigiに一緒に行きましょう!

今回のRubyKaigi2017では、会社に交通費や宿泊費、チケット代を出していただきました。

個人的に、来年のKaigiもぜひ参加したいと考えています。 制度としてカンファレンス参加の援助もありますので、様々なカンファレンスへ仕事として参加することが可能です。

リクルートマーケティングパートナーズではRails/Rubyを書きたい方、一緒にRubyKaigiに参加したい方の入社をお待ちしています。

 

脚注   [ + ]

1. 講演の中では、ここから何ステップか踏んで、Module Builder Patternの紹介に入ります。ぜひスライドも合わせてご覧になってください。

mactkg

Lv.
2
EXP.
263

2017年新卒入社。RailsとiOSとReactとかを書くけど、どれが得意というわけでもない。2017年はRubyの年にしたい。

この執筆者の記事一覧

コメントはこちらのに同意の上、投稿ください。

この記事を読んだ人はこんな記事も読んでいます