はじめに
ScalaでDDDやらクリーンアーキテクチャやらまじめに考えると、リポジトリやサービスのインターフェースの設計に色んな手段が考えられて悩ましいという話。
シンプルなインターフェース
ある意味でシンプルに環境依存も含めて宣言しちゃうパターン。
// 環境依存1 = DBSession
trait Repository[DBSession] {
// 環境依存2 = Future
def findOne(implicit session: DBSession): Future[Either[Throwable, A]]
}
型DBSession
はインフラ層で決定されるとしても、なんだか「依存性逆転の原則」違反がチラチラしてる気がして気になります(違反はしてないとは思うけど)。
当然、環境が変化したらインターフェースも書き換えでしょう(FutureがTryになるとか)。
ただ必要な型が全部見えてるので実装時に楽っちゃ楽だったりします。
モナディックなインターフェース
環境依存は悪だ!Monad
が使えるんだから環境は抽象化できるはずだ!
trait Repository[M[_]] {
def findOne: M[Either[Throwable, A]]
}
2019年5月10日修正:実装しずらかったのでM[_]
の宣言位置を変えました。
これが一番「クリーンアーキテクチャ」的で綺麗な宣言かと思います。
実装時にM[_]
の型はKleisli[Future, DBSession, ?]
のようになることでしょう。
このパターンの一番の問題は、アプリケーション層等での利用時にfor comprehension
内で型揃えが頻発することです。
全部EitherT
でくるんで、Future
じゃないやつはFuture.successful
でくるんで・・・Task
とTry
も?どうすんだこれ?
ExtensibleEffectsなインターフェス
モナディックなインターフェースの型合わせが面倒くさい!インターフェースが綺麗でも実装が汚いのは嫌じゃ!
・・・となった時に現れたのがEffことExtensibleEffects
です。
// ふたたび現れるDBSession ← どうにかする方法を模索中
trait Repository[DBSession] {
type _sessionReader[DBSession] = Reader[DBSession, ?] |= R
// Eff <- from atnos-org/eff
def findOne[R: _sessionReader]: Eff[R, A]
}
また環境がチラチラしてますが、まあ戻り値には影響を及ぼさないので良しとしておいてください。
Eff
自体の詳しい解説は下部の参考先を見てもらうとして、Eff
を用いた場合、モナディックなインターフェースと違い、Monad
であることをキープしたまま実装時のfor comprehension
内での面倒な型揃えを回避することができます。
ただし、インターフェース宣言時にcontext boundを明示したり、関数呼び出し時に型を明示したり、最終的な実行時にrunReader
やらrunEither
やらを必要分記述しなければいけません。・・・あれ?これ逆に面倒くさいんじゃ・・・
おわりに
それで、どれが良いのか結論は?
いや、だから悩んでるって話しでして・・・
- 「シンプルなインターフェース」は自分的にはScalaである必要なくなっちゃうんで無し。
- 「モナディックなインターフェース」は確かに見た目はよいし、クリーンアーキテクチャちっくな雰囲気ある。ただ実装は汚れがち。
- 「ExtensibleEffectsなインターフェス」は実は後の手間よりもインターフェースが汚れやすいのが問題だったりするかも?(環境分、context boundが増える)
for comprehension
はまじ綺麗になる。
「モナディックなインターフェース」と「ExtensibleEffectsなインターフェス」の2択かなぁ。
・・・もう気持ちの問題なのかもって気がしなくもない。
参考先
モナディックなインターフェース関連
ExtensibleEffectsなインターフェス関連