Kotlin + Spring Bootを触ってみたので、Clean ArchitectureでちょっとしたAPIを作ってみました。
- Kotlin: 1.6.10
- SpringBoot: 2.4.4
目次
Spring BootでのDI
そもそもDIとはDependeny Injectionの略で依存性の注入という意味です。DIはオブジェクト間に生じる依存関係をオブジェクト内のコードに直接記述せず、外部から何らかの形で与えるようにする手法です。
@Component
クラスにComponentアノテーションを付与するとDIの対象であることを表します。
そのクラスをDIコンテナが管理してくれるようになります。
他にも@Controllerや@Serviceがありますが、以下の使い分けをします。
- @Controller: MVCでコントローラー層のクラスに付与
- @Service: MVCでサービス層のクラスに付与
- @Repository: MVCでデータ層のクラスに付与
- @Component: MVCに限らず、DIで利用したいクラスへ付与する
@Bean
付与した関数で返却したインスタンスがDIコンテナに登録され、そのインスタンスをDIで使用できるようになります。
@Componentと@Beanの使い分けは以下の記事が参考になりました。
Spring Bootの@Componentと@Beanの違い - grep Tips *
@Autowired
注入されるフィールドに付けます。
DIコンテナはAutowiredアノテーションを付けたフィールドに対して、合致するオブジェクトを探してインジェクションします。
lateinit var
このフィールドへのインジェクションは変数の読み込みと同時に初期化されるのではなく、後からインジェクションされるためvar定義しておく必要があります。
Clean ArchitectureでTODOアプリを意識したAPIを実装してみる
Clean ArchitectureでTODOアプリを意識したAPIを実装してみました。といってもDBも用意せずスタブみたいな感じにしています。メソッドもgetTasks()とgetTask()だけでちょっとしたAPIとなっています。
Enterprise Business Rules
Enterprise Business Rulesではアプリケーション全体の最重要ビジネスルールをカプセル化したドメインを格納しています。
今回はTODOアプリを意識しているので、タスクIDとタイトル、コンテンツを含むTaskドメインとListとで持つTasksドメインを作っています。
・Task.kt
1 2 3 4 5 6 7 8 9 10 11 | package com.example.domain data class TaskId(val value: Int) data class Title(val value: String) data class Content(val value: String) data class Task(val title: Title, val content: Content) data class Tasks(val list: List<Task>) |
Application Business Rules
Application Business Rulesではアプリケーション固有のビジネスルールが含まれ、入出力するデータの流れを調整します。
TaskPortにインターフェイスを定義し、TaskGatewayで具象クラスを実装するようにしています。TaskUseCaseではTaskPortを呼び出すように実装しています。
・TaskPort.kt
1 2 3 4 5 6 7 8 9 10 | package com.example.port import com.example.domain.Task import com.example.domain.TaskId import com.example.domain.Tasks interface TaskPort { fun getTasks(): Tasks fun getTask(taskId: TaskId): Task } |
・TaskUseCase.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | package com.example.usecase import com.example.domain.Task import com.example.domain.TaskId import com.example.port.TaskPort import com.example.domain.Tasks import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component @Component class TaskUseCase() { @Autowired lateinit var taskPort: TaskPort fun getTasks(): Tasks { return taskPort.getTasks() } fun getTask(taskId: TaskId): Task { return taskPort.getTask(taskId) } } |
Interface & Adapter
Interface & Adapterではドメインやユースケースに便利な形式からDBに便利な形式にデータを変換します。
TaskGatewayはTaskPortで定義したinterfaceを実装しています。またTaskDriverで取得したJSONをドメインに変換する処理をしています。
TaskControllerでは逆にドメインをレスポンスの形に変換する処理をしています。
・TaskGateway.kt
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 | package com.example.gateway import com.example.domain.* import com.example.driver.TaskDriver import com.example.driver.TaskJson import com.example.driver.TasksJson import com.example.port.TaskPort import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component @Component class TaskGateway: TaskPort { @Autowired lateinit var taskDriver: TaskDriver override fun getTasks(): Tasks { val tasksJson: TasksJson = taskDriver.getTasks() return tasksJson.toTasks() } override fun getTask(taskId: TaskId): Task { val taskJson: TaskJson = taskDriver.getTask(taskId.value) return taskJson.toTask() } } fun TasksJson.toTasks(): Tasks { return this.list.map { Task(Title(it.title), Content(it.content)) }.let(::Tasks) } fun TaskJson.toTask(): Task { return Task(Title(this.title), Content(this.content)) } |
・TaskController.kt
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 | package com.example.controller import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.RestController import com.example.usecase.TaskUseCase import com.example.domain.Tasks import com.example.domain.Task import com.example.domain.TaskId import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @RestController class TaskController { @Autowired lateinit var taskUseCase: TaskUseCase @GetMapping(value = ["/api/tasks"]) fun getTasks(): ResponseEntity<TasksJson> { return ResponseEntity.ok(taskUseCase.getTasks().toJson()) } @GetMapping(value = ["/api/tasks/{taskId}"]) fun getTask( @PathVariable(value = "taskId", required = true) taskId: Int ): ResponseEntity<TaskJson> { return ResponseEntity.ok(taskUseCase.getTask(TaskId(taskId)).toJson()) } } data class TaskJson(val title: String, val content: String) data class TasksJson(val tasks: List<TaskJson>) fun Tasks.toJson() = this.list.map { it.toJson() }.let(::TasksJson) fun Task.toJson() = TaskJson(this.title.value, this.content.value) |
Framework & Driver
Framework & Driverはフレームワークやツールで構成されています。
今回はスタブでJSON形式の値を返すようにしていますが、本来ならDB操作やAPIリクエストを行う想定です。
・TaskDriver.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | package com.example.driver import com.fasterxml.jackson.annotation.JsonCreator import org.springframework.stereotype.Component @Component class TaskDriver() { fun getTasks(): TasksJson { return TasksJson(listOf(TaskJson("title", "content"), TaskJson("title2", "content2"))) } fun getTask(taskId: Int): TaskJson { return TaskJson("title", "content") } } data class TasksJson @JsonCreator constructor( val list: List<TaskJson> ) data class TaskJson @JsonCreator constructor( val title: String, val content: String ) |