すきま風

勉強したことのメモとか

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が超巨大になっていて、シンプルなレイヤーアーキテクチャで記述したため非常にわかりにくくなってしまって後悔したことからクリーンアーキテクチャを勉強してみようと思い、記事にしてみました。