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はx
とy
を持ったオブジェクトにのみ適用可能で、定義が固定になっています。x
とy
とz
を足すようにしたかったり、price
とtax
を足すようにしたい場合には流用できません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
で、アップロード時の作業などをユーザーが定義したものです。ImageUploader
はShrine
を継承しており、Shrine::Attachment
はModule
を継承したClass
です。
# ImageUploader
class ImageUploader < Shrine
plugin :activerecord
plugin :pretty_location
end
class Shrine
class Attachment < Module
end
#... AttachmentのClassMethodやInstanceMethodが定義されている
#... Plugin機構にするために、少し複雑!(面白いのでぜひ読んでみてください)
end
すなわち、ImageUploader::Attachment.superclass
はModule
です!(感動するところです)
おわりに
今回は、Module Builder Patternについてフォーカスして記事を書きました。少し長くなってしまいましたが、読みながら実際に実行してみることで、理解が深められるかと思います。
Module Builder Patternは、gemを開発するのに便利そうだなと感じています。Moduleを継承するだけのシンプルなパターンですが、覚えておくと表現の幅がグッと広がります。他にModule Builder Patternを使ったgemとしては、dry-equalizerがあります。(@shioyamaさんのブログ記事より) 私も読んでみたのですが、シンプルで読みやすいです。
来年のRubyKaigiに一緒に行きましょう!
今回のRubyKaigi2017では、会社に交通費や宿泊費、チケット代を出していただきました。
リクルートマーケティングパートナーズの提供で会場に来ています: https://t.co/BLSkCdDGjN #rubykaigi
— mactkg (@mactkg) 2017年9月20日
個人的に、来年のKaigiもぜひ参加したいと考えています。 制度としてカンファレンス参加の援助もありますので、様々なカンファレンスへ仕事として参加することが可能です。
リクルートマーケティングパートナーズではRails/Rubyを書きたい方、一緒にRubyKaigiに参加したい方の入社をお待ちしています。