みなさん、こんばんわ。
会社のアドベントカレンダーで、Scalaコードでわかった気になるDDDというブログを書いたのですが、最近、老害を防ぐためにDCIについても勉強中です。
DCIアーキテクチャ - Trygve Reenskaug and James O. Coplien
とりあえず、これを読めということらしいですが、今ひとつ理解できなかったので、
Lean Architecture: for Agile Software Development
を買って読んでます(巻末にScalaのコード例もあってなかなかよさげです)。
この本ではtraitのmix-in方式を紹介しているのですが、この方法はイマイチだと思っているので、別の方法を考えてみたのでさくっと紹介します。
暗黙的型変換と型クラスでの実装
今回は、ECサイトなどの、商品(Product)の購入(purchase)について考えてみる。商品を購入する振る舞いを持つロールを購入者(Purchaser)として定義し、あるコンテキスト(ProductPurchaseContext)内で、継承関係のないユーザ(User)とグループ(Group)に対して、そのロールを適用する方法について考えてみた(ちなみに、実装コードの詳細はあまり書いてないので、イメージとしてとらえてください)。
まず、購入者のロールを表すトレイトがこれ(振る舞いの仕様を記述している部分をMethodless Roleと呼んでいるっぽい)。いわゆる、型クラスです。Aには何でも指定することができる。今回はUserもしくはGroupを指定する。また、購入メソッドであるpurchaseの第一引数にA型が渡される仕様となっている。
// Methodless Role trait Purchaser[A] { def purchase(self: A, product: Product): A } // Data trait User extends Entity[UserId] { // ... } // Data trait Group extends Entity[GroupId] { // ... }
型クラスの実装である、User, Group用のPurchaserの実装を定義する(DCI的にはMethodful Roleと呼んでいるっぽい)。
// Methodful Role object UserPurchaser extends Purchaser[User] { def purchase(self: User, product: Product): User = { // ... } } // Methodful Role object GroupPurchaser extends Purchaser[Group] { def purchase(self: Group, product: Product): Group = { // ... } }
コンテキスト本体はこれ。
implicit def toPurchaserOps
によって、A型(User, Group)に対してpurcheseが追加されます。
実際の振る舞いは、implicit parameterにより、A型に対応したPurchaser[A]の実装が利用されます。
// Context object ProductPurchaseContext { implicit val up = UserPurchaser implicit val gp = GroupPurchaser implicit def toPurchaserOps[A](self: A) (implicit purchaser: Purchaser[A]) = new { def purchase(product: Product): A = purchaser.purchase(self, product) } def purchase(user: User, product: Product) = user.purchase(product) def purchase(group: Group, product: Product) = group.purchase(product) } object Main extends App { // ... import ProductPurchaseContext._ purchase(User("Junichi","Kato"), Product(ProductType.MacPro)) purchase(Group("Capsule Corp"), Product(ProductType.MacPro)) // ... }
コンテキストの内部では、UserとGroupにロールが適用されているので、User#purchaseおよびGroup#purchaseメソッドは呼べます。コンテキスト外部で呼ぶとちゃんとコンパイルエラーになります。 あと、全く継承関係がないモデルに対して、Roleを適用してRoleに定義されたメソッドで操作が可能です(型クラスの実装自体を追加すれば未知のデータに対しても振る舞いを適用することができる)。
Lean Architectureで説明されている方法でも可能だが、既存のインスタンスへのロールの適用が面倒なので、こっちのがよいと思っている。
追記
ろーじーからコメントもらったので、object
ではなくcase class
にしてみた。
object
だとコンテキストがシングルトンになるので微妙じゃね?という感じ。確かに、それはある。コンテキストは、都度作られて実行して破棄されるものだからclass
の方がよいのでは、ってことか。なるほど。
// Context case class ProductPurchaseContext() { implicit val up = UserPurchaser implicit val gp = GroupPurchaser implicit def toPurchaserOps[A](self: A) (implicit purchaser: Purchaser[A]) = new { def purchase(product: Product): A = purchaser.purchase(self, product) } def execute(user: User, product: Product) = user.purchase(product) def execute(group: Group, product: Product) = group.purchase(product) } object Main extends App { // ... ProductPurchaseContext().execute(User("Junichi","Kato"), Product(ProductType.MacPro)) ProductPurchaseContext().execute(Group("Capsule Corp"), Product(ProductType.MacPro)) // ... }