いままで色々 Rails 向けに DCI を実現する gem を作ってきたわけですが(Dicer / BluePrint)、今年もまた新しく考えなおして Rails 向けに DCI を実現する gem を書きました。毎年毎年ほんとよくやりますね。
今年は何気なく作り続けて、いままで活用されていなかった uninclude という gem をついに使って DCI をやってみました。
uninclude にてついては特に解説することもないというか、名は体を表すということで『#unextend
や #uninclude
を Ruby で使えるようになる』という gem です。Refinements などでも実現可能なのですが、Refinements はファイルごとだったりでスコープがわかりづらくなるので使っていません。
RockMotive
2015年の DCI on Ruby は RockMotive です。名前は D.C.I. というバンドのシングル Rock Motive から取っています。iTunes ストアで配信しているので、気になる方は聴いてみるといいです。ちなみに RockMotive はこれを聞きながら開発しました。
RockMotive は Rails を拡張するというより、 Rails に新しい種別を追加します。既存の挙動に何か手を加える訳ではありません。Rails は標準で app 以下に models / controllers / views / assets を用意しますが、これに加えて RockMotive は roles と interactions を追加します。
過去作である BluePrint も同様でしたが、BluePrint の Context は対象の『クラス』に対してロールを指定していたのに比べ、RockMotive は対象の『インスタンス』に対してロールを指定します。
わかりにくいかと思いますので、簡単なコード例を示します。
BluePrint によるロールの指定は以下の様に行われます。
class DealContext < BluePrint::Context
cast User, as: [Shopper, Seller]
end
売買する際、ユーザーは『購入者』と『販売者』に分かれます。が、BluePrint ではクラスに対する拡張しか行えない為、正確に各インスタンスごとの役割を振ることはできません。
対して、RockMotive によるロールの指定は以下の様に行われます。
class DealInteraction < RockMotive::Interaction
def interact(shopper, seller)
end
end
やったぜ!!
落ち着いて話しましょう。『これでいいのか?』と思われるかもしれませんが、これでいいです。これで指定はすべてです。何が起こっているのかを詳しくお話します。
基本は action_args から着想を得ています。 action_args はコントローラーのアクションメソッドの引数名に応じて、 params
から適当な値を引数として渡してくれます。同様の事を RockMotive でも行うことで、多くのコードが省略されています。
RockMotive は #interact
メソッドの定義がされた際、引数などを考慮し適当なロールを検出、割り当てます。この場合は shopper
引数には Shopper
もしくは ShopperRole
のモジュールを、seller
引数には Seller
もしくは SellerRole
モジュールが対応します。
これにより、適切に各インスタンスが『持つべき』振る舞いを持ち、『持つべきでない』振る舞いを持たなくなります。もし、そのように振る舞ってはならないような挙動をさせた場合起こるのは NoMethodError
です。
RockMotive によって書かれるコード
すこし、踏み込んだサンプルを提示しましょう。『あるアイテムを売買する』というシーンです。
まず、必要な ActiveRecord モデルを定義します。このような構成になるでしょう。
コード上はこのような表現になるでしょう。
class User
has_many :item_ownerships, class_name: 'Item::Ownership'
has_many :items, through: :item_ownerships, source: :owner
end
class Item::Ownership
belongs_to :item
belongs_to :owner, class_name: 'User'
end
class Item
has_many :ownerships, class_name: 'Item::Ownership'
has_many :owners, through: :ownerships
end
各アイテムの値段はアイテムごとに固定としましょう。ユーザーはアイテムの所有権を通じてアイテムを所持しています。なのでこの際、正確に販売される物は『アイテム』そのものではなく、『アイテムの所有権』です。
まずは DealInteraction
にそのまま起こるであろう出来事を記述していきましょう。
class DealInteraction < RockMotive::Interaction
def interact(shopper, seller, ownership)
item = ownership.item
shopper.pay(item.price) # a.
seller.get(item.price) # b.
shopper.get(ownership) # c.
end
end
3つの出来事が起こっています。 a. 購入者は金額を払った、 b. 販売者は金額を受け取った、 c. 購入者が所有権を得た。これら3つの出来事を、各ロールに実装していくと、以下のようになります。
module Shopper
def pay(price)
self.points -= price
end
def get(ownership)
ownership.owner = self
end
end
module Seller
def get(price)
self.points += price
end
end
どうでしょうか?特に明瞭だとは感じませんでしたか?残念です。
このコード例では、あえて Shopper#get
と Seller#get
のメソッド名を重複させています。これは、素の ActiveRecord で実現することはできません。やるなら #get_points
とか #get_ownership
とかのメソッド名を編み出すことになるでしょう。僕はもうそういうコンピューター様に大変気を使ったメソッド名を編み出すのに疲れました。#get
でいいじゃない。
これはもちろん極端な例です。通常はもっとほかのやり方があると思います。ですが、僕の考える限りこれはわかりにくいコードではありません。このやり方は、『引数名できちんと意味や役割を表す』ということを強く推奨するようになります。user_a
/ user_b
のような引数名では RockMotive は適切なロールを与えてくれませんからね。
また、これにより副作用的に、モックやスタブによるテストが簡単になります。必要な振る舞いは各 Interaction 内で与えられますから、渡すものとしては正確な User のインスタンスでなくとも、同じような属性を持つシンプルなデータクラスで構わないのです。
今年の DCI によって得られた知見(ポエム)
去年はちょっと忙しかったのでほとんど DCI できていなくて後ろめたい気持ち。もっとデータやコンテキストやインタラクションのことを考えなくては命が危ない。DCI 、もはやなんらかの病として捉えていて、なんとかしてみんなに DCI 大好きになってほしいという強い欲求だけがある。
DCI のことばかり考えていたので、最近どういう言語やパラダイムが流行っているのか全然わかってないけど、あんまりそういう話題を聞かないので寂しい。DDD とかも最近あんまり目にしなくなった。最近その手の話でたくさん目にするのは JS 界隈の MVVM とかの話だけど、あれも難しそうな話題である。特に環境の実装状況にべったりで厳しいみたいなのは、 #unextend
が大抵の言語にないせいで机上の空論となっている DCI に通じるものがあって感慨深い。頑張れ MVVM 。
オブジェクト指向ちゃんとやろう!みたいな話、今でもどこかでされているのかもしれないけど、そういうのはもう一般教養とかのレベルとして認識されているのかもしれない。そうだとしたら悲しいことだと思う。一般教養化するというのは、広く知られることでもあり、深く知らなくていいことだと見切りを付けやすくなることでもあると思っていて、そうなるとあんまり悩んだり考えたりしてもらえなくなるので寂しい。
今年も DCI を頑張っていこうと思います。