パッケージの依存を単方向にするための拡張メソッド

DDDの話をするとしよう*1

IDDD 第9章 モジュールにおいて、モジュール(Scalaだとパッケージ)の依存は極力減らせ。現実的には難しいが、単方向にしたり、親子関係にあるならば双方向を許可する、ということが述べられている。

しかしこれめっちゃ難しい。集約が別集約のファクトリになる場合、単方向という制限はだいたい破綻する。

// userモジュールの集約
class User(id: UserId) {
  def createTask(): Task = // Taskを返すため、taskモジュールに依存
    new Task(TaskId.generate(), id)
}

// taskモジュールの集約
class Task(id: TaskId, ownerId: UserId) // ownerIdとしてUserIdを持つため、userモジュールに依存

そもそもこういった関係のものにファクトリを持たせるな、という話な気もしてくるが、だからといって避けては通れないときもあると思う。ならば親子関係にすれば良いのか、というとそうでもない。そうしちゃうとほとんどがuserモジュールの子になってしまう。たぶん。

そういうときに拡張メソッドを使うことも出来るな、という話をしたいわけです。

さきほどの例だと user.User をこうする。

class User(id: UserId)

要はファクトリを削る。これでtaskモジュールへの依存はなくなる。

で、taskモジュールに以下のようなのを書く。

import user._

package object task {
  implicit class TaskCreator(user: User) {
    def createTask(): Task =
      new Task(TaskId.generate(), user.id)
  }
}

user.User から task.TaskCreator への変換を定義し、そこに拡張メソッドとしてファクトリを持たせてあげる。こうするとファクトリのある場所がtaskモジュールに移動するので、依存が単方向に収まる。

ちなみに使うときは:

import user._
import task._

val user = createUser()
val task = user.createTask() // import task._ をしていないとcreateTaskメソッドが使えない

のように、両方のモジュールをimportしてあげる必要がある。が、むしろこれはtaskモジュール中のオブジェクトを使いますよ、という意味になるので、むしろ好都合だと思ってる。

実践で用いてはいないのだけど、「あれ、こうすればいいんじゃ?」と思った話でした。デメリットは実践してないから知らない。

*1:FGOやったことないです