Spring Boot2 × Kotlin × Gradle5でクリーンアーキテクチャのアプリケーションを構築する (実装編)
build.gradleまで書いた前回記事の続きです。Apiをcallした体のプログラムを記述します。
プロジェクト構成
demo ├── adapters │ ├── build.gradle │ ├── api │ │ ├── build.gradle │ │ └── src.main.kotlin.com.example.demo.api │ │ ├── adapter.SampleApiAdapter.kt │ │ ├── dto.sample.SampleDataResponse.kt │ │ └── repository.sample │ │ ├── SampleDataApiRepository.kt │ │ └── SampleDataApiRepositoryImpl.kt │ ├── persistence │ │ ├── build.gradle │ │ └── src.main.kotlin.com.example.demo.persistence │ └── web │ ├── build.gradle │ └── src.main.kotlin.com.example.demo.web.controller.SampleApiController.kt ├── application │ ├── build.gradle │ └── src.main.kotlin.com.example.demo │ ├── port │ │ ├── input.sample.GetSampleDataUseCase.kt │ │ └── output.sample.GetSampleDataPort.kt │ └── service.sample.SampleDataService.kt ├── configuration │ ├── build.gradle │ └── src.main.kotlin.com.example.demo │ └── DemoApplication.kt ├── domain │ ├── build.gradle │ └── src.main.kotlin.com.example.demo.domain.entity.sample.SampleData.kt ├── build.gradle └── settings.gradle
configuration layer DemoApplication.kt
package com.example.demo import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication class DemoApplication fun main(args: Array<String>) { runApplication<DemoApplication>(*args) }
adapters.web layer SampleApiController.kt
package com.example.demo.adapter.web.controller import com.example.demo.domain.entity.sample.SampleData import com.example.demo.port.input.sample.GetSampleDataUseCase import kotlinx.coroutines.reactor.mono import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Mono @RestController class SampleApiController( private val getSampleDataUseCase: GetSampleDataUseCase ) { @GetMapping("sample") fun getSample(): Mono<SampleData> = mono { getSampleDataUseCase.invoke().value } }
application layerのport.inputに置いてあるuseCaseのinterfaceをcallします。
application layer GetSampleDataUseCase.kt
package com.example.demo.port.input.sample import com.example.demo.domain.entity.sample.SampleData interface GetSampleDataUseCase { suspend fun invoke(): Output data class Output( val value: SampleData ) }
GetSampleDataUseCase.ktの実装クラスをapplication layerのserviceに用意します。
application layer SampleDataService.kt
package com.example.demo.service.sample import com.example.demo.port.input.sample.GetSampleDataUseCase import com.example.demo.port.output.sample.GetSampleDataPort import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import org.springframework.stereotype.Service @Service class SampleDataService( private val getSampleDataPort: GetSampleDataPort ) : GetSampleDataUseCase { override suspend fun invoke() = coroutineScope { val sampleDataDeferred = async { getSampleDataPort.getSampleData() } GetSampleDataUseCase.Output( sampleDataDeferred.await() ) } }
Api call用のinterfaceをapplication layerに用意します。
application layer GetSampleDataPort.kt
package com.example.demo.port.output.sample import com.example.demo.domain.entity.sample.SampleData interface GetSampleDataPort { suspend fun getSampleData(): SampleData class SampleDataNotFoundException : RuntimeException() }
Domain objectはdomain layerのentity配下に用意します
domain layer SampleData.kt
package com.example.demo.domain.entity.sample data class SampleData( val value: String )
実装クラスをadapters.api layerに用意します。
adapters.api layer SampleDataApiAdapter.kt
package com.example.demo.adapter.api.adapter import com.example.demo.adapter.api.repository.sample.SampleDataApiRepository import com.example.demo.domain.entity.sample.SampleData import com.example.demo.port.output.sample.GetSampleDataPort import org.springframework.stereotype.Component @Component class SampleDataApiAdapter( private val sampleDataApiRepository: SampleDataApiRepository ) : GetSampleDataPort { override suspend fun getSampleData(): SampleData { return SampleData( sampleDataApiRepository.fetch().value ) } }
Api call用のRepositoryを用意します。 interfaceも書いていますが、同一layerなので不要かもしれません。
adapters.api layer SampleDataApiRepository.kt
package com.example.demo.adapter.api.repository.sample import com.example.demo.adapter.api.dto.sample.SampleDataResponse interface SampleDataApiRepository { suspend fun fetch(): SampleDataResponse }
package com.example.demo.adapter.api.repository.sample import com.example.demo.adapter.api.dto.sample.SampleDataResponse import org.springframework.stereotype.Repository @Repository class SampleDataApiRepositoryImpl : SampleDataApiRepository { override suspend fun fetch(): SampleDataResponse { // ここにwebClientとかでapi call実装を記載する return SampleDataResponse("sample") } }
layer間を移動するdtoも用意します。
package com.example.demo.adapter.api.dto.sample data class SampleDataResponse( val value: String )
portを通って外部サービスにアクセスする感じですね。 慣れれば理解しやすいですが、ファイル数が多くなるので小さいアプリケーションではここまで分割しなくても良いと思います。 今業務で書いているApiが超巨大になっていて、シンプルなレイヤーアーキテクチャで記述したため非常にわかりにくくなってしまって後悔したことからクリーンアーキテクチャを勉強してみようと思い、記事にしてみました。