Kotlin: Kotlinプロジェクトの実装

最終更新:2026-08-26

設計は完了しました。次はコーディングです。Charlie、Alice、Bobは協力して、OrderProcessorの4層構成のコードを実装します。その構成は、ドメイン層(Order + イベント)、サービス層(ビジネスロジック + コルーチン)、永続化層(リポジトリ)、統合層(HTTPクライアント)です。

1. 学習内容


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. 注文作成の流れ

100%
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などのツールを使えば、これを自動的に強制することができます。

📖 まとめ


📝 練習問題

  1. 初心者 (⭐): OrderItem データクラスと OrderStatus シールクラスを、init ブロック内で検証機能付きで実装してください。ヒント: require()
  2. 中級 (⭐⭐): ステートマシンを用いて遷移を検証し、OrderServiceconfirm()およびship()メソッドを実装してください。ヒント: StateMachine.transition()
  3. 上級 (⭐⭐⭐): 4層構成の OrderProcessor を完全に実装してください:ドメイン(Order + Event + StateMachine)、サービス(OrderService + コルーチン)、永続化(InMemoryRepo)、統合(MockPaymentClient)。 ヒント:第9章の完全な例を参照してください。

← 前へ | 次へ →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%