Kotlin/Ktor 学習用

Q,
Create a singletonの以下のコードはどういう意味でしょうか?
object Resource { val name = "Name" }
A,
このコードは Kotlin におけるシングルトン(Singleton)オブジェクト を定義しています。
object Resource {
val name = "Name"
}
解説
object
キーワード
1. -
class
の代わりにobject
を使うと、その型は 一度だけ生成されるインスタンスになります。 - Java でいう「シングルトンパターン」を Kotlin では言語レベルでサポートしています。
- 自分で
new
したりする必要がなく、アプリ全体で ただ1つのインスタンスが存在します。
val name = "Name"
2. - これは
Resource
というオブジェクトに属する プロパティ。 - つまり
Resource.name
と書いてアクセスできます。
3. 使い方
fun main() {
println(Resource.name) // => "Name"
}
実用的な使い所
-
設定値や定数の管理
object Config { const val API_URL = "https://api.example.com" }
-
ユーティリティ関数の集約
object Logger { fun log(msg: String) = println("[LOG] $msg") }
-
アプリ全体で共有したいリソース
(ただし DB 接続などライフサイクルが必要なものは DI を使うのが推奨)
✅ まとめ
-
object
= Kotlin のシングルトン宣言 -
Resource
はアプリ全体で唯一のインスタンス -
Resource.name
としてアクセスできる
👉 ご質問は「object
の仕組み」自体を知りたい感じですか?
それとも「Ktor プロジェクトでどんな時に object
を使うのか」という観点が欲しいですか?

Q,
この仕組みがあることで、C#のASP.NET MVC Core 2はDIの際にSingleton指定などをしていますが、Ktorではそれがいらなくなるということでしょううか?
A,
すごく良い視点です 👍
Kotlin の object
が「シングルトン」であることと、ASP.NET Core でいう DI コンテナにおける Singleton スコープは似ていますが、完全に同じではない です。
object
のシングルトン
1. Kotlin の -
言語レベルで1インスタンスだけ保証
-
呼び出した瞬間にクラスがロードされ、オブジェクトが生成される
-
アプリケーションのライフサイクル管理はされない
- 例: Ktor アプリ終了時に自動で dispose/close されない
-
スレッドセーフ
👉 「ただ1つのインスタンスが欲しい」場合には便利ですが、
ライフサイクル管理(開始/終了時のリソース解放など)は自分で書く必要があります。
2. ASP.NET Core の DI の Singleton
- DI コンテナがオブジェクトのライフサイクルを管理
-
AddSingleton<T>()
で登録すると、アプリケーション全体で1つのインスタンスを共有 - アプリ終了時には
Dispose()
が呼ばれるなど、リソース解放まで面倒を見てくれる - 同じ仕組みで
Scoped
(リクエストごと)やTransient
(毎回生成)も選べる
👉 つまり 「インスタンスの数の制御」+「ライフサイクル管理」 が組み込まれているのが ASP.NET Core の DI。
3. Ktor での実際
Ktor 自体は軽量フレームワークなので、公式にはフル機能の DI コンテナを持っていません。
代わりに:
-
シンプルに
object
を使う
→ 設定値、ユーティリティ関数、キャッシュなど。 -
ライフサイクルに依存するリソースは ApplicationEvents にフック
object Clients { val http by lazy { HttpClient(CIO) } } fun Application.module() { environment.monitor.subscribe(ApplicationStopping) { Clients.http.close() // 明示的に解放 } }
-
本格的に DI をやりたい場合
→ Koin や Kodein-DI を使うのが一般的です。install(Koin) { modules( module { single { HttpClient(CIO) } single { UserService(get()) } } ) }
まとめ
-
object
があるから 小規模アプリや単純なユーティリティは DI なしで済む - でも ASP.NET Core の
Singleton
のように ライフサイクルまで管理してくれるわけではない - 大規模開発や複雑な依存関係があるなら、Ktor でも Koin などの DI ライブラリを導入するのが主流
👉 確認ですが、ユーザーさんのイメージは 小規模な練習アプリ(体育館予約 API)レベルですか?
それとも 将来的に本番運用(DB、外部API、ジョブ管理など)を見据えて DI を考えたいですか?

Q,
最近のクラウドネイティブなアーキテクチャとの相性を考えるとなるべくDIコンテナは利用せずに作りたい気持ちがあります。(テスタビリティの観点からDIは有効だと思うのでこちらは使うことがあるかもしれません)
A,
いいですね、その方針は全然アリです 👌
「DIコンテナなし+DIパターン(コンストラクタ注入)」で組むと、クラウドネイティブとも相性が良く、テストもしやすいです。Ktor での具体的な設計・実装パターンをサクッとまとめます。
方針
- 依存性はインターフェース+コンストラクタ注入で表現(DI“コンテナ”は使わない)
-
Composition Root(依存の組み立て)は
Application.module
に集約 -
ライフサイクルを持つリソース(DBプール/HTTPクライアントなど)は
ApplicationStopping
でクローズ - 設定は 12-factor(環境変数)+型付き設定オブジェクト
- テストは手動で差し替え(Fake/Stub)か、テスト用の組み立て関数を用意
1) ポート/アダプター(インターフェースで分離)
// domain/ports
interface BookingRepo {
suspend fun find(id: String): Booking?
suspend fun save(b: Booking)
}
// infra/adapters
class BookingRepoPg(private val ds: DataSource): BookingRepo { /* ... */ }
2) サービスはコンストラクタ注入
class BookingService(private val repo: BookingRepo) {
suspend fun get(id: String) = repo.find(id) ?: error("not found")
}
3) Composition Root(Ktor 側で“配線”)
object AppConfig {
val dbUrl: String by lazy { System.getenv("DB_URL") ?: "jdbc:postgresql://localhost:5432/app" }
}
object Resources {
val dataSource: DataSource by lazy {
HikariDataSource(HikariConfig().apply { jdbcUrl = AppConfig.dbUrl })
}
val http: HttpClient by lazy { HttpClient(CIO) { expectSuccess = true } }
}
fun Application.module() {
// 依存の組み立て(コンテナなし)
val bookingRepo: BookingRepo = BookingRepoPg(Resources.dataSource)
val bookingService = BookingService(bookingRepo)
// ライフサイクル管理
environment.monitor.subscribe(ApplicationStopping) {
(Resources.dataSource as? HikariDataSource)?.close()
Resources.http.close()
}
// ルーティングに注入(高階関数で渡すとテストも楽)
routing { bookingRoutes(bookingService) }
}
fun Route.bookingRoutes(svc: BookingService) {
get("/bookings/{id}") {
val id = call.parameters["id"]!!
call.respond(svc.get(id))
}
}
ポイント:Ktor の世界(Application/module)に依存の“配線”を集約=コンストラクタ注入を手でやるだけ。これが Composition Root。
4) 設定を型安全に
data class Settings(val dbUrl: String, val jwtIssuer: String)
fun loadSettings(): Settings = Settings(
dbUrl = System.getenv("DB_URL") ?: error("DB_URL required"),
jwtIssuer = System.getenv("JWT_ISSUER") ?: "gym-booking"
)
-
起動時に失敗させたい必須設定は
lazy
にせず 即評価(失敗の遅延は避ける) - 任意/重い読み込みは
by lazy
で OK
5) テスト容易性(DIパターンだけで十分)
-
ユニットテスト:
BookingRepo
を Fake 実装で差し替え
class BookingRepoFake : BookingRepo { /* メモリに保存 */ }
@Test fun `get returns entity`() = runTest {
val svc = BookingService(BookingRepoFake().apply { /* seed */ })
assertNotNull(svc.get("id1"))
}
-
ルーティングのテスト:
testApplication { application { moduleWith(svc=Fake) } }
のように、組み立て関数をオーバーロードして差し替え可能にする
6) よくある疑問に先回り
- Service Locator は? → NG。グローバルから引くよりコンストラクタ注入を徹底(テスト可能性が段違い)
-
object
乱用は? → 設定/定数/軽量ユーティリティのみ。外部リソースを持つなら停止フックで確実に close -
スコープ管理(リクエスト単位の依存) → Ktor ハンドラに引数で渡すか、
call.attributes
を慎重に使う(基本は関数引数で十分) - 大規模化したら? → それでも手組みでいけますが、配線が煩雑になったら Koin などを“最後に”検討(最初からは不要)
7) ディレクトリ例(小〜中規模)
src/main/kotlin/
app/
Module.kt // Composition Root
Routes.kt
Settings.kt
domain/
model/Booking.kt
ports/BookingRepo.kt
services/BookingService.kt
infra/
db/BookingRepoPg.kt
http/ExternalFooClient.kt
8) 最低限のガードレール
- constructor で必須依存を受け取る(後注入しない)
- 関数は副作用を外に出す(純粋な計算は domain に寄せる)
- 外部I/Oは境界に押し出す(ports/adapters)
この構成なら、DIコンテナを使わずに“クラウドネイティブっぽい疎結合”と“高テスタビリティ”を両立できます。
必要なら、あなたの現在の Ktor リポジトリ構成に合わせて具体的な差分パッチ(ファイル追加/修正)を書きますよ。

Q,
https://kotlinlang.org/docs/idioms.html#use-inline-value-classes-for-type-safe-values Use inline value classes for type-safe values の以下のコードはどういうコードですか?
@JvmInline value class EmployeeId(private val id: String)
@JvmInline value class CustomerId(private val id: String)
A,
これは Kotlin の Inline Value Class(インライン値クラス) を使った「型安全な値の表現」の例です。
@JvmInline
value class EmployeeId(private val id: String)
@JvmInline
value class CustomerId(private val id: String)
1. 何をしているか
-
value class
は「単一のプロパティをラップするクラス」 -
@JvmInline
を付けると JVM 上では ただの String と同じように扱われ、オーバーヘッドが発生しない(インライン展開される) - つまり実行時のコストはほぼゼロで、コンパイル時には「別の型」として扱えます。
2. どういうメリットがあるか
型安全性
例えば String
をそのまま使うと、従業員ID・顧客IDを区別できません:
fun findEmployee(id: String) { ... }
fun findCustomer(id: String) { ... }
val emp = "E-123"
val cus = "C-456"
// 間違えてもコンパイルが通ってしまう
findEmployee(cus)
→ バグの温床になります。
Value Class を使うと区別できる
fun findEmployee(id: EmployeeId) { ... }
fun findCustomer(id: CustomerId) { ... }
val emp = EmployeeId("E-123")
val cus = CustomerId("C-456")
findEmployee(emp) // ✅ OK
findEmployee(cus) // ❌ コンパイルエラー
→ コンパイラが型の取り違えを防いでくれる。
3. 実行時の効率
EmployeeId("E-123")
は実際の JVM バイトコード上ではただの "E-123"
として最適化されるので、
ラッパークラスを作るコストがかかりません。
4. 実務での使い所
- ドメインモデルの ID(UserId, OrderId, BookingId …)
- ユニットの明確化(Meter, Kilometer など、どちらも Double だが区別したい)
- 特殊なルールを持つ文字列/数値のラップ
✅ まとめ
-
@JvmInline value class
は ゼロオーバーヘッドで型安全に扱える「単一値ラッパー」 - コンパイル時に異なる型として区別できるので、ID の取り違えや単位ミスを防げる
- 実務では ドメインID や 単位付き値 に使うのが典型的
👉 ご興味は「Ktor の API 設計で ID を安全に扱う」観点ですか?
それとも「Kotlin の言語機能としての使い方」を深掘りしたい感じですか?

Q, kotlinのスコープ関数をすべて教えてください
A,
はい 👍
Kotlin には「スコープ関数」と呼ばれる便利な関数が標準ライブラリに用意されています。
代表的なものは 5つです。
Kotlin のスコープ関数 一覧
関数 | レシーバの呼び方 | 戻り値 | 主な用途 |
---|---|---|---|
let |
it |
ラムダの結果 | 変換、null 安全処理、メソッドチェーン |
run |
this |
ラムダの結果 | 初期化や複雑な処理をまとめる |
with |
this |
ラムダの結果 | 1つのオブジェクトに対する操作をまとめる |
apply |
this |
レシーバ自身 | オブジェクトの設定(ビルダー風) |
also |
it |
レシーバ自身 | 副作用(ログ出力、デバッグ、チェーン中の確認) |
それぞれの使い方
let
1. val name: String? = "Kotlin"
val length = name?.let {
println("名前は $it")
it.length
} ?: 0
👉 null でなければ処理して結果を返す。
run
2. val message = StringBuilder().run {
append("Hello, ")
append("World!")
toString()
}
👉 複数の処理をまとめて最後の結果を返す。
with
3. val turtle = Turtle()
with(turtle) {
penDown()
repeat(4) {
forward(100.0)
turn(90.0)
}
penUp()
}
👉 1つのオブジェクトに対する操作をスッキリ書ける。
apply
4. val user = User().apply {
name = "Alice"
age = 30
}
👉 初期化・設定をまとめて書く(ビルダーパターン風)。
also
5. val list = mutableListOf("a", "b").also {
println("初期リスト: $it")
}
👉 レシーバ自身を返すのでチェーンに組み込める。ログやデバッグに便利。
まとめ
-
結果を返す系 →
let
,run
,with
-
レシーバ自身を返す系 →
apply
,also
-
this
でアクセス →run
,with
,apply
-
it
でアクセス →let
,also
👉 ユーザーさんはこの5つを 比較表(フローチャート風) で見たいですか?
それとも Ktor プロジェクトの中での具体的な利用例が知りたいですか?

Q,
Mark code as incomplete (TODO) の以下のコードは何ですか?
fun calcTaxes(): BigDecimal = TODO("Waiting for feedback from accounting")
A,
これは Kotlin に用意されている TODO()
関数 を使った「未完成コードのマーカー」です。
fun calcTaxes(): BigDecimal = TODO("Waiting for feedback from accounting")
TODO()
とは?
1. - Kotlin 標準ライブラリにある関数です。
- 呼ばれると必ず
NotImplementedError
を投げます。 - つまり 「ここはまだ実装していない」 ということを明示的に示すための仕組みです。
2. 上記コードの意味
-
calcTaxes()
という関数はBigDecimal
を返す契約になっている。 - けれどまだ会計部門から仕様が来ていないので実装できない。
- そのため一時的に
TODO("Waiting for feedback from accounting")
としておき、呼ばれたら例外を投げるようにしている。
3. 利点
-
コンパイルは通る(未実装でも
TODO()
のおかげで戻り値があるとみなされる) - 実行時に呼ばれたらクラッシュするので、未実装部分を見逃さない
- 引数にメッセージを書いておけば「なぜ未実装なのか」がログに残る
4. 実務での使い所
- プロトタイピングや設計段階で、後から実装する部分を「穴埋め」する
- チーム開発で「ここはAさん担当」という目印にする
- TDD で「テストを先に書き、まだ実装していない関数」をマークする
✅ まとめ
このコードは「calcTaxes
関数は将来 BigDecimal
を返す予定だが、今は未実装で呼ばれると必ず例外が出る」という意味です。
👉 ご希望なら、TODO()
の NotImplementedError
の例外メッセージがどう出るか を実際の出力例付きで解説しますか?

Q,
テスト駆動がしやすくなりそうな感じですね、文字列で番号管理などはめんどくさいと思うのですが、テスト駆動の際、テストコードから未実装の関数には行けそうですが、未実装の関数からテストケースに行く際のわかりやすさを向上させるためのライブラリなどはありますか?
A,
お、鋭いところ突きますね 👍
確かに TODO("123")
のように文字列で管理すると「未実装関数 → どのテスト?」が繋がらなくてつらいです。
Kotlin/TDDで「未実装 ↔ テスト」を繋げる工夫
TODO()
+ テストの失敗メッセージでリンク
1. 標準の TODO()
は「どこが未実装か」は分かりますが、どのテストで落ちたかは IDE/テストランナー側に依存します。
多くの IDE(IntelliJ, VS Code + Kotlin Plugin)なら、テスト結果のスタックトレースからすぐ該当テストにジャンプできるので、ライブラリなしでも割とカバーできます。
@Disabled
)
2. アノテーションを使う(JUnit5 JUnit5 には未実装のテストを「保留」として明示する仕組みがあります:
@Disabled("まだ未実装: 会計ロジック待ち")
@Test
fun `calcTaxes should return proper amount`() {
val result = calcTaxes()
// TODO: 実装後に期待値を入れる
}
👉 これで「テストは存在するが未実装」と明示できます。テストレポートにも表示されるので追跡しやすいです。
pending
や shouldBe
with TODO
3. Spek/Kotest の BDD 系テストフレームワーク(Kotest や Spek)には「未実装のテストケース」を表す構文があります。
Kotest 例
class TaxSpec : StringSpec({
"calcTaxes should compute correct value".config(enabled = false) {
calcTaxes() shouldBe BigDecimal("100")
}
})
あるいは:
class TaxSpec : StringSpec({
"calcTaxes should compute correct value".config(enabled = false) {
TODO("未実装")
}
})
👉 レポートに「skipped」「pending」と出るので、どの仕様が未実装かを一覧で確認できる。
4. Issue Tracker と連携
-
IDEA の TODOコメント機能
// TODO(#123 会計ロジック待ち)
のように書くと、IntelliJ の「TODOタブ」で一覧化できます。 -
**外部の課題管理ツール(Jira, GitHub Issues)**と連動させるチームも多いです。
TODO("See issue #42")
- CIで
TODO
コメントを拾って一覧にする
実務でおすすめの流れ
-
小規模/個人学習 → 標準の
TODO()
と JUnit5@Disabled
で十分 -
チーム開発 → Kotest の
pending
や CIでの TODO収集を組み合わせると「未実装タスク一覧」が管理しやすい -
規模が大きい場合 → GitHub Issues や Jira にタスク化して
TODO("issue-123")
でリンク
✅ まとめ
-
Kotlin 標準の
TODO()
は 未実装 → テスト の方向は問題なし -
未実装関数 → テストケース の見通しを良くするには:
-
JUnit5
@Disabled
や Kotest の pending 機能で「未実装テスト」を明示する - IDEA の TODO 一覧や 課題管理ツール連携でトレーサビリティを確保する
-
JUnit5
👉 ユーザーさんの用途は「学習用にシンプルに分かればいい」ですか?
それとも「チーム開発で未実装タスクとテストケースをちゃんと追跡したい」感じですか?

Q,
以下のkotlinのClass layoutの説明に沿ったサンプルクラスを実装してください
(日本語訳)
クラスのレイアウト
クラスの内容は以下の順序で記述してください:
プロパティ宣言と初期化子ブロック
セカンダリコンストラクタ
メソッド宣言
コンパニオンオブジェクト
メソッド宣言をアルファベット順や可視性で並べ替えたり、通常のメソッドと拡張メソッドを分離したりしないでください。代わりに、関連するものをまとめて配置し、クラスを上から下へ読む人が処理の論理を追えるようにします。順序(上位概念を先に記述するか、その逆か)を決め、一貫して適用してください。
ネストされたクラスは、それらを使用するコードの近くに配置します。外部での使用を意図し、クラス内部で参照されない場合は、コンパニオンオブジェクトの後に配置します。
原文
Class layout
The contents of a class should go in the following order:
Property declarations and initializer blocks
Secondary constructors
Method declarations
Companion object
Do not sort the method declarations alphabetically or by visibility, and do not separate regular methods from extension methods. Instead, put related stuff together, so that someone reading the class from top to bottom can follow the logic of what's happening. Choose an order (either higher-level stuff first, or vice versa) and stick to it.
Put nested classes next to the code that uses those classes. If the classes are intended to be used externally and aren't referenced inside the class, put them in the end, after the companion object.
A,
/**
* Sample class laid out per the guideline:
* 1) Properties & init blocks
* 2) Secondary constructors
* 3) Methods (group related ones together; mix regular/extension helpers)
* 4) Companion object
* 5) Nested classes intended for external use and NOT referenced inside the class
*/
class TaskQueue(
private val store: Persistence,
private val now: () -> Long = { System.currentTimeMillis() }
) {
// --- 1) Property declarations & initializer blocks -----------------------
private val inMemoryBuffer = mutableListOf<Task>() // small write-through buffer
init {
// basic invariant checks
require(now() > 0) { "Clock must return a positive epoch millis." }
}
// --- 2) Secondary constructors ------------------------------------------
/**
* Convenience constructor for an in-memory queue.
*/
constructor() : this(InMemoryPersistence())
// --- 3) Methods (public API first, then helpers; related things together) -
/** Enqueue a task and return its assigned id. */
fun enqueue(payload: String, priority: Int = DEFAULT_PRIORITY): String {
val task = Task(
id = Ids.newId(),
payload = payload,
priority = priority.coerceIn(MIN_PRIORITY..MAX_PRIORITY),
enqueuedAt = now()
)
inMemoryBuffer.add(task)
// write-through to persistence
store.save(task)
return task.id
}
/** Try to dequeue the highest-priority task (stable by time). */
fun dequeue(): Task? {
// merge view: buffer + persisted (demonstration purpose)
val merged = (store.all() + inMemoryBuffer).distinctBy { it.id }.toMutableList()
val next = merged.pickHighestPriority()
if (next != null) {
store.delete(next.id)
inMemoryBuffer.removeIf { it.id == next.id }
}
return next
}
/** Peek current size (not strictly consistent; demo intent). */
fun size(): Int = (store.all() + inMemoryBuffer).distinctBy { it.id }.size
/** Clear everything. */
fun clear() {
store.clear()
inMemoryBuffer.clear()
}
// ----- Helpers: keep them close to the methods that use them -------------
/** Member *extension* on a mutable list of Task: pick highest priority, FIFO on ties. */
private fun MutableList<Task>.pickHighestPriority(): Task? =
this.maxWithOrNull(compareBy<Task> { it.priority }.thenBy { it.enqueuedAt })
// --- 4) Companion object -------------------------------------------------
companion object {
const val MIN_PRIORITY = 0
const val MAX_PRIORITY = 10
const val DEFAULT_PRIORITY = 5
/** Simple id generator kept with the type that uses it. */
private object Ids {
fun newId(): String = buildString(16) {
append(System.currentTimeMillis().toString(16))
append('-')
append((0..Int.MAX_VALUE).random().toString(16))
}
}
}
// Put nested classes next to the code that uses them:
// These are referenced inside TaskQueue, so keep them here (not at the end).
/** Persistence port (interface) used by TaskQueue. */
interface Persistence {
fun save(task: Task)
fun all(): List<Task>
fun delete(id: String)
fun clear()
}
/** Simple in-memory adapter; used by the secondary ctor and methods above. */
private class InMemoryPersistence : Persistence {
private val map = LinkedHashMap<String, Task>()
override fun save(task: Task) { map[task.id] = task }
override fun all(): List<Task> = map.values.toList()
override fun delete(id: String) { map.remove(id) }
override fun clear() { map.clear() }
}
/** Value object used internally and in public API returns. */
data class Task(
val id: String,
val payload: String,
val priority: Int,
val enqueuedAt: Long
)
}
// --- 5) Nested types intended for external use and NOT referenced inside ----
// (If you had such types, place them after the companion object of the class.)
// For demonstration, imagine an externally-consumed DTO unrelated to internals:
/** Example: A lightweight projection that external modules might use. */
data class TaskSummary(val id: String, val priority: Int)

Q,
先ほど生成してもらったクラスのコメントを整理して、先ほど書いてもらったコードと以下の項目の対応がわかりやすいようにしてください
- Property declarations and initializer blocks
- Secondary constructors
- Method declarations
- Companion object
A,
/**
* Class layout sample that maps clearly to the requested order:
* 1) Property declarations & initializer blocks
* 2) Secondary constructors
* 3) Method declarations (related ones grouped together)
* 4) Companion object
*/
class TaskQueue(
private val store: Persistence,
private val now: () -> Long = { System.currentTimeMillis() }
) {
// ------------------------------------------------------------
// [1] Property declarations and initializer blocks
// ------------------------------------------------------------
private val inMemoryBuffer = mutableListOf<Task>() // small write-through buffer
init {
require(now() > 0) { "Clock must return a positive epoch millis." }
}
// ------------------------------------------------------------
// [2] Secondary constructors
// ------------------------------------------------------------
/** Convenience in-memory queue constructor. */
constructor() : this(InMemoryPersistence())
// ------------------------------------------------------------
// [3] Method declarations
// (public API first; helpers placed near their callers)
// ------------------------------------------------------------
/** Enqueue a task and return its id. */
fun enqueue(payload: String, priority: Int = DEFAULT_PRIORITY): String {
val task = Task(
id = Ids.newId(),
payload = payload,
priority = priority.coerceIn(MIN_PRIORITY..MAX_PRIORITY),
enqueuedAt = now()
)
inMemoryBuffer.add(task)
store.save(task) // write-through
return task.id
}
/** Dequeue highest-priority (stable by time) task; null if none. */
fun dequeue(): Task? {
val merged = (store.all() + inMemoryBuffer).distinctBy { it.id }.toMutableList()
val next = merged.pickHighestPriority()
if (next != null) {
store.delete(next.id)
inMemoryBuffer.removeIf { it.id == next.id }
}
return next
}
/** Approximate queue size. */
fun size(): Int = (store.all() + inMemoryBuffer).distinctBy { it.id }.size
/** Clear all tasks. */
fun clear() {
store.clear()
inMemoryBuffer.clear()
}
/** Helper: pick highest priority, FIFO on ties. */
private fun MutableList<Task>.pickHighestPriority(): Task? =
this.maxWithOrNull(compareBy<Task> { it.priority }.thenBy { it.enqueuedAt })
// ------------------------------------------------------------
// [4] Companion object
// ------------------------------------------------------------
companion object {
const val MIN_PRIORITY = 0
const val MAX_PRIORITY = 10
const val DEFAULT_PRIORITY = 5
/** Simple id generator kept with the type that uses it. */
internal object Ids {
fun newId(): String = buildString(16) {
append(System.currentTimeMillis().toString(16))
append('-')
append((0..Int.MAX_VALUE).random().toString(16))
}
}
}
// (Nested classes are placed next to usage sites; these are referenced above)
interface Persistence {
fun save(task: Task)
fun all(): List<Task>
fun delete(id: String)
fun clear()
}
private class InMemoryPersistence : Persistence {
private val map = LinkedHashMap<String, Task>()
override fun save(task: Task) { map[task.id] = task }
override fun all(): List<Task> = map.values.toList()
override fun delete(id: String) { map.remove(id) }
override fun clear() { map.clear() }
}
data class Task(
val id: String,
val payload: String,
val priority: Int,
val enqueuedAt: Long
)
}

Q, kotlinの型エイリアスとvalue classの使い分けを教えてください
A,
いい質問!ざっくり言うと――
- typealias = ただの別名(読みやすさのため)。新しい型にはならない
- value class = 1つの値を包む実体のある“別の型”(型安全)。JVM では**ゼロコスト(ほぼ)**で扱える
使い分けの指針(チートシート)
目的 | 使うべきもの | 理由 / 例 |
---|---|---|
可読性を上げたい(長い型名や関数型に別名を付けたい) | typealias | 型はそのまま。例:typealias Headers = Map<String, List<String>> / typealias OnClick = (Event) -> Unit
|
ドメインID・単位などを取り違えたくない(型安全にしたい) | value class |
UserId と OrderId を別型にできる。ゼロコストで安全。 |
バリデーションや補助メソッドを型に紐づけたい | value class | メソッド/プロパティを持てる(1フィールド制約あり)。 |
既存ライブラリの型に別名だけ付けたい(挙動はそのまま) | typealias | 互換性を保ったまま、読みやすくできる。 |
APIの引数/戻り値で誤用をコンパイル時に防ぎたい | value class | 異なる型として扱われ(Kotlin内)、取り違えを防ぐ。 |
具体例
typealias(別名だけ)
typealias Headers = Map<String, List<String>>
typealias UserJson = String
typealias OnTaxCalculated = (amount: BigDecimal) -> Unit
// 実体は元の型と完全に同じ(オーバーロードの区別も不可)
fun send(h: Headers) { /* Map<String, List<String>> と同じ */ }
- メリット: 短くなる・意図が伝わる
- デメリット: 型安全性は増えない(
Headers
もMap<…>
も同じ型として扱われる)
value class(型安全 & ゼロコスト)
@JvmInline
value class UserId(val value: String) {
fun masked() = value.takeLast(6).padStart(value.length, '*')
}
@JvmInline
value class Yen(val amount: Long)
@JvmInline
value class Usd(val amount: Long)
// 誤用をコンパイルで防げる(Kotlin から呼ぶ場合)
fun pay(id: UserId, price: Yen) { /* ... */ }
// こういう取り違えはエラーになる
// pay(UserId("u1"), Usd(100)) // 型が違うのでコンパイルエラー
-
メリット: 別型として扱われる/メソッド・不変条件を載せられる/JVM で実行時オーバーヘッドほぼゼロ
-
注意:
- 1フィールドのみ(主プロパティは1つ)
-
Nullable・ジェネリック・
Any
/インターフェースに渡す時はボクシングが起きうる(=小さなオーバーヘッド) - Java から見ると中身の型として見えることが多く、Java境界では型安全が落ちる可能性あり
- シリアライザは対応が必要(
kotlinx.serialization
なら@Serializable
を付ければOK)
Ktor 文脈での実用例
良い(value class):
@JvmInline
value class BookingId(val value: String)
get("/bookings/{id}") {
val id = call.parameters["id"]?.let(::BookingId) ?: return@get call.respond(HttpStatusCode.BadRequest)
// 以降は BookingId として安全に扱える
}
読みやすさだけ欲しい(typealias):
typealias JsonBody = String
suspend fun respondJson(call: ApplicationCall, body: JsonBody) { call.respondText(body, ContentType.Application.Json) }
まとめ
-
読みやすくしたいだけ →
typealias
別名を付けるだけ。安全性は増えない。 -
ドメインの“意味”を型で表し、誤用を防ぎたい →
value class
1フィールドを包む別の型として扱える。Kotlin内部では型安全、JVMではほぼゼロコスト。
迷ったら:ID・通貨・単位 → value class、長い型名/関数型の短縮 → typealias が定石です。

Q,
Ktorなど軽量フレームワークと組み合わせて使えるDrizzle的ORM を求めています
A,
いいですね、その条件ならまずは Exposed が本命です。Ktorと相性が良く、“コードでスキーマを書く × 型安全なDSL” という意味で Drizzle っぽい 使い心地になります。軽量派なら Ktorm、非同期/R2DBCを使いたいなら Komapper も有力です。
イチ押し(Drizzle的)
-
Exposed (JetBrains)
- コード主導(
object Users : Table()
)でスキーマ定義 - 型安全なDSL(
select
,insert
,update
,join
…) - Ktorと素直に統合可(HikariCP + JDBC)
- マイグレーションは Flyway/Liquibase を併用(Drizzle Kitの役割)
- コード主導(
代替
-
Ktorm
- さらに軽量・シンプルなDSL。data classで扱いやすい
- スキーマ管理は別途(Flyway/Liquibase)
-
Komapper
- 注釈 + KSPでメタモデル生成、DSLあり
- R2DBC対応(ノンブロッキングI/Oが欲しいときの第一候補)
- JPAほど重くないのに型安全が強い
Ktor × Exposed の最小セットアップ例
build.gradle.kts
)
Gradle(dependencies {
implementation("io.ktor:ktor-server-core-jvm:2.3.12")
implementation("io.ktor:ktor-server-netty-jvm:2.3.12")
implementation("com.zaxxer:HikariCP:5.1.0")
implementation("org.jetbrains.exposed:exposed-core:0.54.0")
implementation("org.jetbrains.exposed:exposed-dao:0.54.0")
implementation("org.jetbrains.exposed:exposed-jdbc:0.54.0")
implementation("org.postgresql:postgresql:42.7.4") // DBに合わせて
// マイグレーション(任意)
implementation("org.flywaydb:flyway-core:10.18.2")
}
Application.kt
など)
DB接続(import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import org.jetbrains.exposed.sql.Database
fun connectDatabase() {
val cfg = HikariConfig().apply {
jdbcUrl = System.getenv("DATABASE_URL") ?: "jdbc:postgresql://localhost:5432/app"
username = System.getenv("DB_USER") ?: "app"
password = System.getenv("DB_PASSWORD") ?: "password"
maximumPoolSize = 10
}
Database.connect(HikariDataSource(cfg))
}
スキーマ & クエリ(Drizzle感のあるDSL)
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
object Users : Table("users") {
val id = long("id").autoIncrement()
val name = varchar("name", 50)
val email = varchar("email", 255).uniqueIndex()
override val primaryKey = PrimaryKey(id)
}
fun createAndQuery() = transaction {
// 本番は SchemaUtils ではなく Flyway/Liquibase を推奨
SchemaUtils.create(Users)
Users.insert {
it[name] = "Alice"
it[email] = "alice@example.com"
}
Users.selectAll()
.limit(10)
.map { it[Users.id] to it[Users.name] }
}
Ktor で使う(簡易)
fun Application.module() {
connectDatabase()
routing {
get("/users") {
val users = org.jetbrains.exposed.sql.transactions.transaction {
Users.selectAll().map {
mapOf("id" to it[Users.id], "name" to it[Users.name])
}
}
call.respond(users)
}
}
}
⚠️ Exposed/KtormはJDBCベースでブロッキングです。Ktorでは
withContext(Dispatchers.IO)
で囲むか、DAO層でまとめてIOディスパッチャに寄せるのが無難です。完全非同期に振るなら Komapper + R2DBC を検討。
マイグレーション(Drizzle Kit 相当)
Flyway を使うのが鉄板です。
src/main/resources/db/migration/
└─ V1__create_users.sql
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE
);
// 起動時に実行
import org.flywaydb.core.Flyway
fun migrate() {
val url = System.getenv("DATABASE_URL") ?: "jdbc:postgresql://localhost:5432/app"
val user = System.getenv("DB_USER") ?: "app"
val pass = System.getenv("DB_PASSWORD") ?: "password"
Flyway.configure()
.dataSource(url, user, pass)
.locations("classpath:db/migration")
.load()
.migrate()
}
module()
の冒頭で migrate(); connectDatabase()
の順に呼ぶと安定します。
こう選ぶとラク
-
まずは Exposed + Flyway
→ Drizzle派に一番しっくり。Ktorと小さく始めやすい - さらに軽くシンプル → Ktorm
- 非同期/R2DBC・型安全生成 → Komapper (R2DBC)
必要なら、今のKtor雛形にそのまま貼れる最小リポ構成(build.gradle.kts
, Application.kt
, Tables.kt
, migrations/
)まで一気に書き出します。どのDB(Postgres / MySQL / SQLite)でいきますか?