Ktor と Exposed でバックエンドを書いてる
Ktor と Exposed を使って Kotlin でバックエンドを書いていていくつか自分なりのプラクティスを入れ込めたのでメモしておく
プロジェクト構成
├── Procfile
├── README.md
├── app.json
├── build.gradle
├── docker-compose.yml
├── gradle.properties
├── resources
│ ├── application.conf
│ └── db
│ └── migration : migration files
├── settings.gradle
├── src
│ ├── Application.kt
│ ├── application
│ │ └── feature : feature based packaging
│ │ ├── datastore : interface and DTO
│ │ ├── service : Application Service (e.g managing transaction)
│ │ └── usecase : business logic
│ ├── datastore
│ │ └── model : model based packaging
│ ├── domain
│ ├── lib : Internal Libraries
│ │ └── transaction
| └── router
│ ├── Routing.kt : all passes are defined
│ └── context : package by context
│ ├── Router.kt
│ ├── controller : controllers
│ ├── request : (e.g post request)
│ └── response : response data type
├── system.properties
└── test : WordSpec styled test
特徴的なのは、 - application layer - datastore layer - domain layer - router layer
の4つのレイヤー
Domain Driven Development 的な話でいえば、 Domain Model が永続化の方法まで知っている場合もある。
最近は、UseCase を中心にすえて DataStore は単純にDB操作の技術詳細を知っているという捉え方をしている。
だいたいアーキテクチャは、こんな感じ
Router -> Controller あたりの細かい実装はこんな感じ
// Router
fun Route.someRoute() {
val controller = Controller()
// 以下のレスポンスが `/some/hoge` のようになる
routing("some") {
get("/hoge") {
val result = controller.getSomething()
call.respond(result)
}
post("/fuga") {
val request = call.receive<DoSomethingRequest>()
val result = controller.doSomething(request)
call.respond(result)
}
}
}
// Controller
class Controller {
fun getSomething(): GetSomethingResponse {
val result = GetSomethingUseCase().execute()
return GetSomethingResponse(
data = result.data
)
}
fun doSomething(request: DoSomethingRequest): DoSomethingResponse {
val args = DoSomethingUseCaseArgs()
return DoSomethingUseCase().execute(args)
}
}
Router -> Controller のやりとりは、 Request モデル
Controller -> Router のやりとりは、 Response モデル
Controller -> UseCase のやりとりは、 UseCaseArgs モデル
UserCase -> Controller のやりとりは、UserCaseResult モデル
でやりとりをする。
Routing
routing("some") {
get("/hoge") {
val result = controller.getSomething()
call.respond(result)
}
}
これでも綺麗に整理できているのだが、基本的にはグループ単位でファイルを分けたいなどもあり一覧性が悪くなってしまう。
Routing.kt
という以下のようなファイルを用意してそこに全てのパスの構造を書くことにした
object Routing {
object Some {
private const val root = "some"
const val hoge = "${root}/hoge/"
}
object Fuga {
private const val root = "fuga"
const val piyo = "${root}/piyo"
}
}
fun Route.someRoute() {
get(Routing.Some.hoge) {
// do something
}
}
// 別ファイル
fun Route.fugaRoute() {
get(Routing.Fuga.piyo) {
// do something
}
}
こうすることで全体の構成の一覧性を維持したまま、Router のファイルは別のファイルとして管理できる
Test
この辺は好みの話だとは思うが、kotest を使って WordSpec 形式で書いてる。
WordSpec はこんな感じ
"メソッド名" should {
"できることを日本語で書く" {
// assertion
}
}
DBのテスト
DB のテストは、DBを用意してもいいんだが、しゅっと用意したかったので embedded-postgres を使っている。
今のところこれで困ってはいない。
まとめ
特にまとめる内容もないけれど、Ktor と Exposed で今どういう感じの工夫を入れてるかというのメモした。