Kotlin: Kotlinプロジェクトの実装
最終更新:2026-08-26
設計は完了しました。次はコーディングです。Charlie、Alice、Bobは協力して、OrderProcessorの4層構成のコードを実装します。その構成は、ドメイン層(Order + イベント)、サービス層(ビジネスロジック + コルーチン)、永続化層(リポジトリ)、統合層(HTTPクライアント)です。
1. 学習内容
- ドメイン層:
data class/sealed class/StateMachine - サービス層:
OrderService+ 非同期処理 +Flowイベントストリーム - パーシステンス層:Spring Data R2DBC + コルーチン・リポジトリ
- 統合層:外部サービスへの呼び出し用 Ktor HTTP クライアント
- Alice/Bob/Charlieによる共同開発ワークフロー
2. あるチームの実際の物語
(1) 課題:役割分担が不明確なことが対立の原因となっている
AliceとBobが同時に OrderService を変更したため、Gitの競合が10件発生しました。根本原因はレイヤーごとの分業がなく、両者が同じファイルを編集していたことです。
(2) レイヤーベースの分割ソリューション
TEXT
📖 参照専用
Charlie: Domain layer (Order, OrderEvent, StateMachine)
Alice: Service layer (OrderService, business logic)
Bob: Repository + Integration layer (data access, HTTP clients)
レイヤードアーキテクチャは、チームの役割分担にも対応します。各層を独立して開発し、インターフェースの契約によって競合を最小限に抑えます。
3. ドメイン層
(1) 主要なエンティティ
KOTLIN
// Domain entities - pure Kotlin, no framework dependency
data class OrderItem(
val sku: String,
val quantity: Int,
val unitPrice: Double
) {
val subtotal: Double get() = quantity * unitPrice
init {
require(quantity > 0) { "Quantity must be positive" }
require(unitPrice >= 0) { "Price must be non-negative" }
}
}
data class Address(
val street: String,
val city: String,
val country: String
)
data class Order(
val id: String,
val customerId: String,
val items: List<OrderItem>,
val shippingAddress: Address?,
var status: OrderStatus,
var trackingCode: String?,
val createdAt: String,
var updatedAt: String?
) {
val total: Double get() = items.sumOf { it.subtotal }
init {
require(id.startsWith("ORD-")) { "Invalid order ID format" }
require(items.isNotEmpty()) { "Order must have at least one item" }
}
}
(2) 状況と出来事
KOTLIN
sealed class OrderStatus {
data class Pending(val createdAt: String) : OrderStatus()
data class Confirmed(val confirmedAt: String) : OrderStatus()
data class Shipped(val trackingCode: String, val shippedAt: String) : OrderStatus()
data class Delivered(val deliveredAt: String) : OrderStatus()
data class Cancelled(val reason: String, val cancelledAt: String) : OrderStatus()
}
sealed class OrderEvent {
abstract val orderId: String
abstract val timestamp: String
data class Created(
override val orderId: String,
val customerId: String,
val total: Double,
override val timestamp: String
) : OrderEvent()
data class Confirmed(override val orderId: String, override val timestamp: String) : OrderEvent()
data class Shipped(override val orderId: String, val trackingCode: String, override val timestamp: String) : OrderEvent()
data class Delivered(override val orderId: String, override val timestamp: String) : OrderEvent()
data class Cancelled(override val orderId: String, val reason: String, override val timestamp: String) : OrderEvent()
}
(3) ステートマシン
KOTLIN
object OrderStateMachine {
fun transition(current: OrderStatus, event: OrderEvent): OrderStatus = when {
current is OrderStatus.Pending && event is OrderEvent.Confirmed ->
OrderStatus.Confirmed(event.timestamp)
current is OrderStatus.Confirmed && event is OrderEvent.Shipped ->
OrderStatus.Shipped(event.trackingCode, event.timestamp)
current is OrderStatus.Shipped && event is OrderEvent.Delivered ->
OrderStatus.Delivered(event.timestamp)
current is OrderStatus.Pending && event is OrderEvent.Cancelled ->
OrderStatus.Cancelled(event.reason, event.timestamp)
else ->
throw IllegalStateException("Invalid transition from $current with $event")
}
fun canTransition(current: OrderStatus, event: OrderEvent): Boolean = when {
current is OrderStatus.Pending && event is OrderEvent.Confirmed -> true
current is OrderStatus.Confirmed && event is OrderEvent.Shipped -> true
current is OrderStatus.Shipped && event is OrderEvent.Delivered -> true
current is OrderStatus.Pending && event is OrderEvent.Cancelled -> true
else -> false
}
}
4. サービス層
(1) OrderService
KOTLIN
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class OrderService(
private val repository: OrderRepository,
private val eventStore: EventRepository,
private val paymentClient: PaymentClient,
private val inventoryClient: InventoryClient
) {
suspend fun createOrder(request: CreateOrderRequest): Order {
val order = Order(
id = generateOrderId(),
customerId = request.customerId,
items = request.items.map { OrderItem(it.sku, it.quantity, it.unitPrice) },
shippingAddress = request.shippingAddress,
status = OrderStatus.Pending(request.timestamp),
trackingCode = null,
createdAt = request.timestamp,
updatedAt = null
)
repository.save(order)
eventStore.append(OrderEvent.Created(order.id, order.customerId, order.total, request.timestamp))
return order
}
suspend fun confirmOrder(orderId: String, timestamp: String): Order {
val order = repository.findById(orderId) ?: throw NoSuchElementException("Order $orderId not found")
val event = OrderEvent.Confirmed(orderId, timestamp)
val newStatus = OrderStateMachine.transition(order.status, event)
order.status = newStatus
order.updatedAt = timestamp
repository.save(order)
eventStore.append(event)
return order
}
suspend fun shipOrder(orderId: String, trackingCode: String, timestamp: String): Order {
val order = repository.findById(orderId) ?: throw NoSuchElementException("Order $orderId not found")
val event = OrderEvent.Shipped(orderId, trackingCode, timestamp)
val newStatus = OrderStateMachine.transition(order.status, event)
order.status = newStatus
order.trackingCode = trackingCode
order.updatedAt = timestamp
repository.save(order)
eventStore.append(event)
return order
}
fun orderEvents(orderId: String): Flow<OrderEvent> = flow {
eventStore.findByOrderId(orderId).forEach { emit(it) }
}
private fun generateOrderId(): String = "ORD-${System.currentTimeMillis()}"
}
(2) リクエスト/レスポンス DTO
KOTLIN
data class CreateOrderRequest(
val customerId: String,
val items: List<CreateItemRequest>,
val shippingAddress: AddressRequest?,
val timestamp: String
)
data class CreateItemRequest(val sku: String, val quantity: Int, val unitPrice: Double)
data class AddressRequest(val street: String, val city: String, val country: String)
data class OrderResponse(
val id: String,
val customerId: String,
val total: Double,
val status: String,
val items: List<OrderItemResponse>,
val trackingCode: String?,
val createdAt: String,
val updatedAt: String?
)
data class OrderItemResponse(val sku: String, val quantity: Int, val unitPrice: Double, val subtotal: Double)
fun Order.toResponse() = OrderResponse(
id, customerId, total, status::class.simpleName ?: "Unknown",
items.map { OrderItemResponse(it.sku, it.quantity, it.unitPrice, it.subtotal) },
trackingCode, createdAt, updatedAt
)
5. パーシステンス層
(1) リポジトリインターフェース
KOTLIN
interface OrderRepository {
suspend fun save(order: Order)
suspend fun findById(id: String): Order?
suspend fun findAll(): List<Order>
suspend fun deleteById(id: String)
}
interface EventRepository {
suspend fun append(event: OrderEvent)
fun findByOrderId(orderId: String): List<OrderEvent>
}
(2) インメモリ実装(開発段階)
KOTLIN
class InMemoryOrderRepository : OrderRepository {
private val storage = mutableMapOf<String, Order>()
override suspend fun save(order: Order) { storage[order.id] = order }
override suspend fun findById(id: String) = storage[id]
override suspend fun findAll() = storage.values.toList()
override suspend fun deleteById(id: String) { storage.remove(id) }
}
class InMemoryEventRepository : EventRepository {
private val events = mutableListOf<OrderEvent>()
override suspend fun append(event: OrderEvent) { events.add(event) }
override fun findByOrderId(orderId: String) = events.filter { it.orderId == orderId }
}
6. 統合層
(1) HTTP クライアントインターフェース
KOTLIN
interface PaymentClient {
suspend fun processPayment(orderId: String, amount: Double): PaymentResult
}
interface InventoryClient {
suspend fun checkAvailability(sku: String, quantity: Int): Boolean
suspend fun reserve(sku: String, quantity: Int): ReservationResult
}
sealed class PaymentResult {
data class Success(val transactionId: String) : PaymentResult()
data class Failed(val reason: String) : PaymentResult()
}
sealed class ReservationResult {
data class Reserved(val reservationId: String) : ReservationResult()
data class Unavailable(val reason: String) : ReservationResult()
}
// Simulated implementations
class MockPaymentClient : PaymentClient {
override suspend fun processPayment(orderId: String, amount: Double) =
PaymentResult.Success("TXN-${orderId.substring(4)}")
}
class MockInventoryClient : InventoryClient {
override suspend fun checkAvailability(sku: String, quantity: Int) = quantity < 10_000
override suspend fun reserve(sku: String, quantity: Int) =
ReservationResult.Reserved("RES-${sku.substring(4)}")
}
7. 4層モデルにおける責任の比較
| レイヤー | 役割 | 依存関係の方向 | 代表的なクラス | テスト戦略 |
|---|---|---|---|---|
| ドメイン | ビジネスルールとエンティティ | 依存関係なし | Order、OrderStatus、StateMachine | 純粋なユニットテスト |
| サービス | ビジネスプロセスのオーケストレーション | → ドメイン/永続化/統合 | OrderService | 依存関係をモック化したユニットテスト |
| 永続化 | データアクセス | → ドメイン | OrderRepository、EventRepository | 統合テスト (Testcontainers) |
| 統合 | 外部サービスへの呼び出し | → ドメイン | PaymentClient、InventoryClient | 契約テスト + モック |
(1) リポジトリ実装戦略の比較
| フェーズ | 実施内容 | メリット | デメリット |
|---|---|---|---|
| 開発 | InMemory* | 設定不要、高速 | 永続化なし |
| テスト | Testcontainers | 実際のデータベース、再現性あり | Docker が必要 |
| 開発 | Spring Data R2DBC | ネイティブのコルーチン対応、高性能 | 接続プールの設定が必要 |
(2) 状態遷移規則表
| 現在の状態 | 許容されるイベント | 目標状態 |
|---|---|---|
| 保留中 | 確定 | 確定 |
| 保留中 | キャンセル | キャンセル |
| 確認済み | 発送済み | 発送済み |
| 発送済み | 配達済み | 配達済み |
| その他のコンボ | 無効 | — |
8. 注文作成の流れ
sequenceDiagram
participant Client
participant Controller
participant Service
participant Repo
participant Events
participant Payment
participant Inventory
Client->>Controller: POST /api/v1/orders
Controller->>Service: createOrder(request)
Service->>Inventory: checkAvailability(sku, qty)
Inventory-->>Service: Available
Service->>Repo: save(order)
Repo-->>Service: Saved
Service->>Events: append(Created event)
Service->>Payment: processPayment(orderId, total)
Payment-->>Service: Success
Service->>Repo: save(order status=Confirmed)
Service->>Events: append(Confirmed event)
Service-->>Controller: Order
Controller-->>Client: 201 Created
9. 完全な例
▶ サンプル:OrderProcessorの全体実装
KOTLIN
// ============================================
// OrderProcessor - End-to-End Implementation
// Feature: Full order lifecycle with state machine
// ============================================
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
// --- Domain (from sections above) ---
data class OrderItem(val sku: String, val quantity: Int, val unitPrice: Double) {
val subtotal: Double get() = quantity * unitPrice
}
sealed class OrderStatus {
data class Pending(val at: String) : OrderStatus()
data class Confirmed(val at: String) : OrderStatus()
data class Shipped(val trackingCode: String, val at: String) : OrderStatus()
data class Cancelled(val reason: String, val at: String) : OrderStatus()
}
sealed class OrderEvent {
abstract val orderId: String
data class Created(override val orderId: String, val total: Double, val at: String) : OrderEvent()
data class Confirmed(override val orderId: String, val at: String) : OrderEvent()
data class Shipped(override val orderId: String, val code: String, val at: String) : OrderEvent()
data class Cancelled(override val orderId: String, val reason: String, val at: String) : OrderEvent()
}
data class Order(
val id: String, val customerId: String, val items: List<OrderItem>,
var status: OrderStatus, var trackingCode: String?, val createdAt: String, var updatedAt: String?
) { val total: Double get() = items.sumOf { it.subtotal } }
object StateMachine {
fun next(current: OrderStatus, event: OrderEvent): OrderStatus = when {
current is OrderStatus.Pending && event is OrderEvent.Confirmed -> OrderStatus.Confirmed(event.at)
current is OrderStatus.Confirmed && event is OrderEvent.Shipped -> OrderStatus.Shipped(event.code, event.at)
current is OrderStatus.Pending && event is OrderEvent.Cancelled -> OrderStatus.Cancelled(event.reason, event.at)
else -> error("Invalid: $current + $event")
}
}
// --- Repository ---
class OrderRepo {
private val db = mutableMapOf<String, Order>()
fun save(o: Order) { db[o.id] = o }
fun find(id: String) = db[id]
fun findAll() = db.values.toList()
}
class EventRepo {
private val events = mutableListOf<OrderEvent>()
fun append(e: OrderEvent) { events.add(e) }
fun find(orderId: String) = events.filter { it.orderId == orderId }
}
// --- Service ---
class OrderService(private val orders: OrderRepo, private val events: EventRepo) {
private var counter = 0
fun create(customerId: String, items: List<OrderItem>, at: String): Order {
val order = Order("ORD-${++counter}", customerId, items, OrderStatus.Pending(at), null, at, null)
orders.save(order)
events.append(OrderEvent.Created(order.id, order.total, at))
return order
}
fun confirm(id: String, at: String): Order {
val order = orders.find(id) ?: error("Not found: $id")
val event = OrderEvent.Confirmed(id, at)
order.status = StateMachine.next(order.status, event)
order.updatedAt = at
orders.save(order)
events.append(event)
return order
}
fun ship(id: String, code: String, at: String): Order {
val order = orders.find(id) ?: error("Not found: $id")
val event = OrderEvent.Shipped(id, code, at)
order.status = StateMachine.next(order.status, event)
order.trackingCode = code
order.updatedAt = at
orders.save(order)
events.append(event)
return order
}
fun cancel(id: String, reason: String, at: String): Order {
val order = orders.find(id) ?: error("Not found: $id")
val event = OrderEvent.Cancelled(id, reason, at)
order.status = StateMachine.next(order.status, event)
order.updatedAt = at
orders.save(order)
events.append(event)
return order
}
fun history(id: String) = events.find(id)
}
// --- Demo ---
fun main() {
val service = OrderService(OrderRepo(), EventRepo())
println("=== OrderProcessor Development Demo ===\n")
// Create orders
val o1 = service.create("CUST-001", listOf(OrderItem("SKU-A", 3, 9.99), OrderItem("SKU-B", 1, 149.99)), "2026-07-13T10:00:00Z")
println("Created: ${o1.id} | Total: \$${o1.total} USD | Status: ${o1.status}")
val o2 = service.create("CUST-002", listOf(OrderItem("SKU-C", 2, 5_000.0)), "2026-07-13T10:01:00Z")
println("Created: ${o2.id} | Total: \$${o2.total} USD | Status: ${o2.status}")
// Confirm
val confirmed = service.confirm(o1.id, "2026-07-13T10:05:00Z")
println("\nConfirmed: ${confirmed.id} | Status: ${confirmed.status}")
// Ship
val shipped = service.ship(o1.id, "TRK-ABC123", "2026-07-13T11:00:00Z")
println("Shipped: ${shipped.id} | Tracking: ${shipped.trackingCode} | Status: ${shipped.status}")
// Cancel
val cancelled = service.cancel(o2.id, "Customer request", "2026-07-13T10:10:00Z")
println("Cancelled: ${cancelled.id} | Reason: ${(cancelled.status as OrderStatus.Cancelled).reason}")
// Event history
println("\n=== Event History: ${o1.id} ===")
service.history(o1.id).forEach { println(" $it") }
}
出力:
TEXT
📖 参照専用
=== OrderProcessor Development Demo ===
Created: ORD-1 | Total: $179.96 USD | Status: Pending(at=2026-07-13T10:00:00Z)
Created: ORD-2 | Total: $10000.0 USD | Status: Pending(at=2026-07-13T10:01:00Z)
Confirmed: ORD-1 | Status: Confirmed(at=2026-07-13T10:05:00Z)
Shipped: ORD-1 | Tracking: TRK-ABC123 | Status: Shipped(trackingCode=TRK-ABC123, at=2026-07-13T11:00:00Z)
Cancelled: ORD-2 | Reason: Customer request
=== Event History: ORD-1 ===
Created(orderId=ORD-1, total=179.96, at=2026-07-13T10:00:00Z)
Confirmed(orderId=ORD-1, at=2026-07-13T10:05:00Z)
Shipped(orderId=ORD-1, code=TRK-ABC123, at=2026-07-13T11:00:00Z)
❓ よくある質問
Q ドメイン層はフレームワークに依存すべきですか?
A いいえ。ドメイン層は、Springやその他のフレームワークへの依存を一切持たない純粋なKotlinコードです。これにより、ドメインロジックを独立してテストし、再利用できるようになります。
Q サービス層はドメインオブジェクトを返すのが適切か、それともDTOを返すのが適切か?
A サービス層はドメインオブジェクトを返し、コントローラーがそれらをDTOに変換する。各層の責任を明確にする:サービス層はHTTPを認識しない。
Q ステートマシンはどこに配置すべきですか?
A ドメイン層に配置します。ステートマシンは中核となるビジネスロジックであり、いかなる外部層にも依存してはなりません。純粋なKotlinのシールクラスを使用して実装してください。
Q 並行する状態更新にはどのように対処すればよいですか?
A データベースの楽観的ロック(バージョンフィールド)または分散ロックを使用します。コルーチンのレベルでは、
Mutex を使用して共有状態を保護してください。本番環境では、データベースによるロックを推奨します。Q 統合レイヤーではどのHTTPクライアントを使用すべきですか?
A Kotlinプロジェクトの場合は、Ktor Client(コルーチン-native)の使用をお勧めします。Javaのエコシステムでは、Spring WebClientも使用可能です。どちらも
suspendに対応しています。Q チームがレイヤーごとに作業を分担するようにするにはどうすればよいですか?
A コードレビューのルール:コントローラーはリポジトリに直接アクセスしない、サービスはHTTPについて認識しない、ドメイン層はフレームワークに依存しない。ArchUnitなどのツールを使えば、これを自動的に強制することができます。
📖 まとめ
- ドメイン層:純粋なKotlin、データクラス+シールクラス+ステートマシン、フレームワークへの依存なし
- サービス層:ビジネスロジック + 状態遷移 + イベントの公開、リポジトリ層および統合層への呼び出し
- パーシステンス層:インターフェースの抽象化 + メモリ内実装(開発環境) + データベース実装(本番環境)
- 統合レイヤー:HTTPクライアントインターフェース + モック実装(開発環境) + Ktor/Spring実装(本番環境)
- 階層的な役割分担:Charlieがドメインを担当し、Aliceがサービスを担当し、Bobが永続化・統合を担当する
- ステートマシン + イベントソーシング:状態の変化ごとにイベントを記録し、完全な監査証跡をサポートします
📝 練習問題
- 初心者 (⭐):
OrderItemデータクラスとOrderStatusシールクラスを、initブロック内で検証機能付きで実装してください。ヒント:require() - 中級 (⭐⭐): ステートマシンを用いて遷移を検証し、
OrderServiceのconfirm()およびship()メソッドを実装してください。ヒント:StateMachine.transition() - 上級 (⭐⭐⭐): 4層構成の OrderProcessor を完全に実装してください:ドメイン(Order + Event + StateMachine)、サービス(OrderService + コルーチン)、永続化(InMemoryRepo)、統合(MockPaymentClient)。 ヒント:第9章の完全な例を参照してください。