クリーンアーキテクチャを勉強して思ったこと

アプリケーション設計をする時にDDDを採用して進める場合、Layered ArchitectureやClean Architectureなど色んな実装手法が選択肢として上がってくる。
ちょっと勉強した感じ、これらの基本は各Layerでは呼び出す側のレイヤーしか意識せず呼び出されるものが入ってきたり、直接関係のないレイヤーに関しては徹底的に排除することで疎結合を保つ仕組みになっている。しかし、実装を進めていくと全レイヤーを通してどうしても外部リソースへのアクセス用クラスなどが見え隠れするため若干やりたいことと実装がズレているのではないかとモヤモヤする。
例えば、CSVを取り込んでDBに登録するようなバッチを作るとして、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
/** * 外部リソースアクセス用クラスを格納するための格納クラス */ trait EntityIOContext /** * ScalaCSV用の格納クラス */ class EntityIOContextForScalaCSV(reader: CSVReader.type, writer: CSVWriter.type) extends EntityIOContext /** * ScalikeJDBC用の格納クラス */ class EntityIOContextForJDBC(session: DBSession) extends EntityIOContext /** * サンプルのドメインサービス */ trait SampleDataService { def readData(path: String)(implicit ctx: EntityIOContext): Try[SampleData] def storeData(data: SampleData)(implicit ctx: EntityIOContext): Try[SampleDataId] } /** * DI用コンテナ */ trait UsesSampleDataService { def sampleDataService: SampleDataService } /** * ユースケースで使用するインプットデータ */ case class ImportSampleDataInputData(path: String) /** * ユースケースで使用するアウトプットデータ */ case class ImportSampleDataOutputData(sampleDataId: SampleDataId) /** * SampleDataインポート用Presenter */ trait ImportSampleDataPresenter { def complete(outputData: ImportSampleDataOutputData): Unit } /** * DIコンテナ */ trait UsesImportSampleDataPresenter { def importSampleDataPresenter: ImportSampleDataPresenter } /** * SampleDataインポート用ユースケース */ trait ImportSampleDataUseCase { def handler(input: SampleInputData)(implicit ctxForScalaCsv: EntityIOContextForScalaCsv, ctxForJDBC: EntityIOContextForJDBC): Unit } /** * インタラクタ */ trait ImportSampleDataInteractor extends UsesSampleDataService with UsesImportSampleDataPresenter { override def handler(input: SampleInputData)(implicit ctxForScalaCsv: EntityIOContextForScalaCsv, ctxForJDBC: EntityIOContextForJDBC): Unit = for { sampleData <- sampleDataService.readData(input.path)(ctxForScalaCsv) sampleDataId <- sampleDataService.sampleDataService(sampleData)(ctxForJDBC) } yield importSampleDataPresenter(SampleDataOutputData(sampleDataId)) } |
と書いた時(なんか書き間違いあってコンパイルできないかもしれないけど・・・)、ここでインタラクタに着目するとScalaCSV用とJDBC用のそれぞれの格納クラスを引数に取らないといけないし、メソッドを呼び出す時も呼び出される側でせっかく「implicit句」を入れてくれているのに引数に暗黙的に渡されず一々指定をしなければいかなくてちょっと微妙である。
で、色々と考えたらこうなった
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
/** * 外部リソースアクセス用クラスを格納するための格納クラス */ trait EntityIOContext /** * ScalaCSV用の格納クラス */ class EntityIOContextForScalaCSV(reader: CSVReader.type, writer: CSVWriter.type) extends EntityIOContext /** * ScalikeJDBC用の格納クラス */ class EntityIOContextForJDBC(session: DBSession) extends EntityIOContext /** * クラスを全て格納するためのコンテナクラス */ case class EntityIOContextContainer(ctxForScalaCsv: Option[EntityIOContextForScalaCsv] = None, ctxForJDBC: Option[EntityIOContextForJDBC] = None) /** * サンプルのドメインサービス */ trait SampleDataService { def readData(path: String)(implicit ctr: EntityIOContextContainer): Try[SampleData] def storeData(data: SampleData)(implicit ctr: EntityIOContextContainer): Try[SampleDataId] } ・・・ 省略 ・・・ /** * SampleDataインポート用ユースケース */ trait ImportSampleDataUseCase { def handler(input: SampleInputData)(implicit ctr: EntityIOContextContainer): Unit } /** * インタラクタ */ trait ImportSampleDataInteractor extends UsesSampleDataService with UsesImportSampleDataPresenter { override def handler(input: SampleInputData)(implicit ctr: EntityIOContextContainer): Unit = for { sampleData <- sampleDataService.readData(input.path) sampleDataId <- sampleDataService.sampleDataService(sampleData) } yield importSampleDataPresenter(SampleDataOutputData(sampleDataId)) } |
これだと外部リソースをユースケースやドメインサービスは意識しなくていいので、コードがほんの気持ちシンプルになった気がする。EntityIOContextContainerに格納されているものはリポジトリで初めて中から取り出され、目的に沿って適切に利用するようにした。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
trait EntityIOAdapter { def withDBSession[A](ctr: EntityIOContextContainer)(f: DBSession => A): A = { ctx.ctxForJDBC.getOrElse(throw new Exception("DBSession is not defined")) match { case EntityIOContextOnJDBC(dbSession) => dbSession case _ => throw new IllegalStateException(s"Unexpected context.") } } def withCsvReader[A](ctr: EntityIOContextContainer)(f: CSVReader.type => A): A = { ... } def withCsvWriter[A](ctr: EntityIOContextContainer)(f: CSVWriter.type => A): A = { ... } } trait SampleDataRepository { def getCSVData(path: String)(implicit ctr: EntityIOContextContainer): SampleData = withCsvReader(ctr) { implicit reader => ... } def createSampleData(sampleData)(implicit ctr: EntityIOContextContainer): SampleDataId = withDBSession(ctr) { implicit session => ... } } |
これだと、実際に外部を意識しているのはリポジトリだけなのでなんかスッキリするしストレージが変わった時にも最小限の変更で良さそうである。
まぁ、とは言っても本格的にこれで実装したことないから正直うまくいくかは謎だけどどこかで試したいね。さて、メモとしてブログに考えを書いたし、そろそろ寝るか。
“クリーンアーキテクチャを勉強して思ったこと” への1件のフィードバック