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 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の紹介に入ります。ぜひスライドも合わせてご覧になってください。。
24 25 26 27 28 29 30 31 | # 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
は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 end end |
これだけです。使うときはインスタンス化して使います。
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 ) 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を使いまわせることではないかと感じています。例えば緯度・経度を表す場合でもそのまま使うことができます。
1 2 3 4 5 6 7 8 | 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 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_location end |
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の紹介に入ります。ぜひスライドも合わせてご覧になってください。 |
※ コメントはこちらのに同意の上、投稿ください。