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同士の足し算の機能を追加する AddableというModuleを考えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # 足し算の機能を追加するAddable Modulemodule Addable def +(other) self.class.new(x + other.x, y + other.y) endend# xとyを持つPoint StructにAddableをincludeして# 足し算の機能を追加するPoint = Struct.new(:x, :y) do include Addable # オーバーライドして定義もできる def +(other) puts ":+ called" super(other) endend# 使用例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の紹介に入ります。ぜひスライドも合わせてご覧になってください。。
24 25 26 27 28 29 30 31 | # Attributeの名前が異なるStructにincludeすると…ItemPrice = Struct.new(:price, :tax) do include Addableend# 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 はClassである(Module.class #=> Class)ことを利用して、Moduleを動的に定義するModule Builderを作ります。具体的には、Moduleを継承したクラスを作ることで実装します。コード例を次に示します。
1 2 3 4 5 6 7 8 9 10 11 | # 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 endend |
これだけです。使うときはインスタンス化して使います。
12 13 14 15 16 17 18 19 20 | # Module Builderを使うときは、initializeしてincludeするPoint3d = Struct.new(:x, :y, :z) do include AdderBuilder.new(:x, :y, :z)enda = Point3d.new(42, 4, 10)b = Point3d.new(10, 1, 100)p a + b #=> #<struct Point3d x=52, y=5, z=110> |
この作成方法の面白いところは、様々な用途でModuleを使いまわせることではないかと感じています。例えば緯度・経度を表す場合でもそのまま使うことができます。
1 2 3 4 5 6 7 8 | City = Struct.new(:latitude, :longitude) do include AdderBuilder.new(:latitude, :longitude)endtokyo = 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 Builder Patternを紹介しましたが、講演の中で、@shioyamaさんはModule Designについて3つの要素を挙げていました。
Module Builder Patternは、これら3つの要素をうまくまとめ上げたパターンになっていると思います。まずは最初の例のように、具体的なユースケースで落とし込んでModuleを作り、そこから抽象化をしてインタフェースに落とし込み、define_method などを使いながらModule Builderを仕上げていくと良いのかなと考えています。
最後に、私がRubyKaigi前に観測したModule Builder Patternを紹介します。
最近Railsでファイルアップロードを行うのに、Shrineというgemを使っています。Shrineを使うのに少しgemの中身を読んでいたところ、Module Builder Patternと出会い、RubyKaigiでのトークに話がつながりました。
次のコードはShrineを使い、Userモジュールにavatarという名前の画像を持たせる場合のコードです。
1 2 3 4 | class User < ApplicationRecord # Module Builder Pattern!!!!!! include ImageUploader::Attachment.new(:avatar)end |
ImageUploaderはユーザーが定義したClassで、アップロード時の作業などをユーザーが定義したものです。ImageUploaderはShrineを継承しており、Shrine::AttachmentはModuleを継承したClassです。
1 2 3 4 | class ImageUploader < Shrine plugin :activerecord plugin :pretty_locationend |
1 2 3 4 5 6 7 | 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さんのブログ記事より) 私も読んでみたのですが、シンプルで読みやすいです。
今回のRubyKaigi2017では、会社に交通費や宿泊費、チケット代を出していただきました。
個人的に、来年のKaigiもぜひ参加したいと考えています。 制度としてカンファレンス参加の援助もありますので、様々なカンファレンスへ仕事として参加することが可能です。
リクルートマーケティングパートナーズではRails/Rubyを書きたい方、一緒にRubyKaigiに参加したい方の入社をお待ちしています。
脚注
| 1. | ↑ | 講演の中では、ここから何ステップか踏んで、Module Builder Patternの紹介に入ります。ぜひスライドも合わせてご覧になってください。 |
※ コメントはこちらのに同意の上、投稿ください。