Service で DB から find する際のキーによって戻り値型を Option と List で変えたい場合にどうしたらいいか考えてみたけどどうなんでしょうという話

Service で DB からあるキーでデータをひろいたい。かつ、キーによって戻したいデータの型が Option だったり List だったりするとする。Service にキーごとのメソッドを生やせばいいけどダサいしたくさん書くのがいやなのでジェネリックに書けないか考えてみたけどいい方法なのかわからないので晒す。コードは Scala です。

※追記あり。いろいろご指摘をいただきました。

DAO にはキーごとのメソッドを生やしてみた。

class ActionDAOImpl @Inject() (db: DB) extends ActionDAO {

  def collection = db.collection[JSONCollection]("action")

  override def readById(id: UUID): Future[Option[Action]] = collection.find(Json.obj("id" -> id)).one[Action]

  override def readByEntryId(entryId: UUID): Future[List[Action]] =
    collection.find(Json.obj("entryId" -> entryId)).cursor[Action]().collect[List]()

}

Service でも同様にキーごとにメソッドを生やせばいいけど、なんとなく気が進まない。Service には当然 DB 触らせたくないので、拾ってくるデータは DB 非依存の型である必要がある。ので、find の戻り型を渡すわけにもいかない。かつ型は堅牢に扱いたい。

結果をジェネリックなクラスでラップすることを考えた。ResultWrapper[T] というクラスを作った。単にAnyなプロパティを持って、その型が型パラメータと一致していれば値を返してくれるというだけのやつ。

case class ResultWrapper[T](value: Any) {

  def result: T = value match {
    case v: T => v
    case _ => throw new RuntimeException
  }

}

これを使って、Service にジェネリックなデータ取得メソッドを書く。

sealed trait Key[T]
case object Id extends Key[Option[Action]]
case object EntryId extends Key[List[Action]]

class ActionService @Inject() (actionDao: ActionDAO) {

  def retrieve[T](key: Key[T], id: UUID): Future[ResultWrapper[T]] = key match {
    case Id => actionDao.readById(id).map(ResultWrapper[T](_))
    case EntryId => actionDao.readByEntryId(id).map(ResultWrapper[T](_))
  }

}

case class でキーに対応するクラス Id と EntryId を作り、その型パラメータにキーに応じた戻り値の型を渡しておく。これで、いずれかのキーを渡すと、戻り値が必ず確定する。

このクラスを引数として渡して、型推定により T が決定されると、ResultWrapper の型が決定するので、あるキーで値を取得した場合の型が保証される。

val act: Future[Option[Action]] = actionService.retrieve(ActionService.Id, id).map(_.result)

とりあえず目標は達成されてるけど、これが良い実装なのかわからなくて晒した。一般的にもっときれいに書けるのではないかという気がする。

※追記1 最終的な解決案

単純にキーの型でオーバーロードする方法。簡単かつ上記の実装だと静的に型検査できなかった問題を回避できる。実装もシンプルだし、キーを宣言的に扱える、メソッドは単一で入力型に合わせて出力型を変更できる・静的に安全、という要求を満たせる。

case class Id(key: UUID)
case class EntryId(key: UUID)

class ActionService @Inject() (actionDao: ActionDAO) {

  def retrieve(id: Id): Future[Option[Action]] = actionDao.readById(id.key)

  def retrieve(entryId: EntryId): Future[List[Action]] = actionDao.readByEntryId(entryId.key)

}

※追記2 元の実装の改善

元の実装だと、Exception を投げちゃってるのがあまりスマートではなかった。ただ、結果型をつかってモナディックにラップするのは常套手段ということで、Scala っぽく書くなら ResultWrapper#result の型を Either にしてエラーをラップするのがそれっぽいという話をした。

case class ResultWrapper[T](value: Any) {

  def result: Either[HogeException, T] = value match {
    case v: T => Right(v)
    case _ => Left(new HogeException)
  }
}

おしまい。

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中