Kotlin: شرح تطوير مشاريع كوتلن

آخر تحديث: 2026-08-26

التصميم انتهى - الآن وقت الكود. يتعاون Charlie و Alice و Bob لتنفيذ كود الطبقات الأربع لـ OrderProcessor: طبقة النطاق (طلب + أحداث)، طبقة الخدمة (منطق الأعمال + كوروتينات)، طبقة الاستمرارية (مستودع)، وطبقة التكامل (عملاء HTTP).

1. ما ستتعلمه


2. قصة فريق حقيقية

(1) المشكلة: تقسيم العمل غير الواضح يسبب تعارضات

عدّل كل من Alice و Bob OrderService في نفس الوقت - 10 تعارضات Git. السبب الجذري: عدم وجود تقسيم عمل طبقي يعني أن كليهما كان يحرر نفس الملف.

(2) حل التقسيم الطبقي للعمل

TEXT 📖 للعرض فقط
Charlie: طبقة النطاق (Order، OrderEvent، StateMachine)
Alice:   طبقة الخدمة (OrderService، منطق الأعمال)
Bob:     طبقة المستودع + التكامل (الوصول للبيانات، عملاء HTTP)

البنية المعمارية الطبقية = تقسيم العمل الطبقي. كل طبقة تتطور بشكل مستقل، مع عقود الواجهة تقلل التعارضات.


3. طبقة النطاق

(1) الكيانات الأساسية

KOTLIN
// كيانات النطاق - كوتلن بحت، بدون تبعية على إطار عمل
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) DTOs الطلب/الاستجابة

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()
}

// تنفيذات محاكاة
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. مقارنة مسؤوليات الطبقات الأربع

الطبقة المسؤولية اتجاه التبعية الفئات النموذجية استراتيجية الاختبار
النطاق قواعد الأعمال والكيانات بدون تبعيات Order، OrderStatus، StateMachine اختبارات وحدة بحتة
الخدمة تنسيق عمليات الأعمال ← النطاق/الاستمرارية/التكامل OrderService اختبارات وحدة مع تبعيات محاكاة
الاستمرارية الوصول للبيانات ← النطاق OrderRepository، EventRepository اختبارات تكامل (Testcontainers)
التكامل استدعاءات الخدمات الخارجية ← النطاق PaymentClient، InventoryClient اختبارات عقد + محاكاة

(1) مقارنة استراتيجية تنفيذ المستودع

المرحلة التنفيذ المميزات العيوب
التطوير InMemory* بدون تهيئة، سريع بدون استمرارية
الاختبار Testcontainers قاعدة بيانات حقيقية، قابلة للتكرار يتطلب Docker
الإنتاج Spring Data R2DBC أصلي للكوروتينات، أداء عالي يحتاج تهيئة تجمع الاتصالات

(2) جدول قواعد الانتقال بين الحالات

الحالة الحالية الحدث المسموح الحالة المستهدفة
Pending Confirmed Confirmed
Pending Cancelled Cancelled
Confirmed Shipped Shipped
Shipped Delivered Delivered
تركيبات أخرى غير قانونية -

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. مثال كامل

KOTLIN
// ============================================
// OrderProcessor - تنفيذ شامل
// الميزة: دورة حياة طلب كاملة مع آلة حالة
// ============================================

import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking

// --- النطاق (من الأقسام أعلاه) ---
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")
    }
}

// --- المستودع ---
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 }
}

// --- الخدمة ---
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)
}

// --- عرض توضيحي ---
fun main() {
    val service = OrderService(OrderRepo(), EventRepo())

    println("=== عرض تطوير OrderProcessor ===\n")

    // إنشاء طلبات
    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}")

    // تأكيد
    val confirmed = service.confirm(o1.id, "2026-07-13T10:05:00Z")
    println("\nConfirmed: ${confirmed.id} | Status: ${confirmed.status}")

    // شحن
    val shipped = service.ship(o1.id, "TRK-ABC123", "2026-07-13T11:00:00Z")
    println("Shipped: ${shipped.id} | Tracking: ${shipped.trackingCode} | Status: ${shipped.status}")

    // إلغاء
    val cancelled = service.cancel(o2.id, "Customer request", "2026-07-13T10:10:00Z")
    println("Cancelled: ${cancelled.id} | Reason: ${(cancelled.status as OrderStatus.Cancelled).reason}")

    // سجل الأحداث
    println("\n=== سجل الأحداث: ${o1.id} ===")
    service.history(o1.id).forEach { println("  $it") }
}

الإخراج:

TEXT 📖 للعرض فقط
=== عرض تطوير OrderProcessor ===

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

=== سجل الأحداث: 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)

10. أمثلة عملية سريعة

▶ مثال: Domain Layer (طبقة نقية بدون إطار)

KOTLIN
// في طبقة النطاق - بدون Spring، بدون إطار
// فقط كوتلن بحت

// 1. نموذج النطاق
data class Order(
    val id: OrderId,
    val customerId: CustomerId,
    val items: List<OrderItem>,
    val status: OrderStatus
) {
    fun total(): Long = items.sumOf { it.subtotal() }
}

// 2. Value Objects
@JvmInline
value class OrderId(val value: String) {
    init { require(value.matches(Regex("^ORD-\\d{6}$"))) }
}

@JvmInline
value class CustomerId(val value: String) {
    init { require(value.isNotBlank()) }
}

enum class OrderStatus { PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED }

data class OrderItem(val productId: String, val quantity: Int, val unitPrice: Long) {
    init { require(quantity > 0); require(unitPrice >= 0) }
    fun subtotal(): Long = quantity * unitPrice
}

// 3. State Machine - sealed class
sealed class OrderEvent {
    data class Created(val order: Order) : OrderEvent()
    data class ItemAdded(val orderId: OrderId, val item: OrderItem) : OrderEvent()
    data class Confirmed(val orderId: OrderId) : OrderEvent()
    data class Shipped(val orderId: OrderId) : OrderEvent()
    data class Cancelled(val orderId: OrderId) : OrderEvent()
}

// 4. Domain Service
class OrderDomainService {
    fun calculateDiscount(order: Order, customerTier: String): Long {
        val baseTotal = order.total()
        return when (customerTier) {
            "GOLD" -> baseTotal / 10  // 10% خصم
            "SILVER" -> baseTotal / 20  // 5%
            else -> 0
        }
    }

    fun canBeCancelled(order: Order): Boolean =
        order.status in setOf(OrderStatus.PENDING, OrderStatus.CONFIRMED)
}

// 5. Repository Interface (في طبقة النطاق)
interface OrderRepository {
    suspend fun save(order: Order): Order
    suspend fun findById(id: OrderId): Order?
}

// لاحظ: لا توجد تبعيات Spring في طبقة النطاق!

**الإخراج:

TEXT 📖 للعرض فقط

▶ مثال: Infrastructure Layer

KOTLIN
// في طبقة البنية التحتية - مع Spring/JPA
import jakarta.persistence.*
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

// JPA Entity
@Entity
@Table(name = "orders")
data class OrderJpaEntity(
    @Id
    var id: String = "",

    @Column(name = "customer_id")
    var customerId: String = "",

    @Column(nullable = false)
    var status: String = "PENDING",

    @Column(name = "created_at")
    var createdAt: java.time.Instant = java.time.Instant.now()
)

// JPA Repository
interface OrderJpaRepository : JpaRepository<OrderJpaEntity, String> {
    fun findByCustomerId(customerId: String): List<OrderJpaEntity>
    fun findByStatus(status: String): List<OrderJpaEntity>
}

// Mapper - يحول بين Domain و JPA
@Component
class OrderMapper {
    fun toDomain(entity: OrderJpaEntity): Order =
        Order(
            id = OrderId(entity.id),
            customerId = CustomerId(entity.customerId),
            items = emptyList(),  // تبسيط: تحميل منفصل
            status = OrderStatus.valueOf(entity.status)
        )

    fun toEntity(order: Order): OrderJpaEntity =
        OrderJpaEntity(
            id = order.id.value,
            customerId = order.customerId.value,
            status = order.status.name
        )
}

// تنفيذ Repository
@Repository
class JpaOrderRepository(
    private val jpaRepo: OrderJpaRepository,
    private val mapper: OrderMapper
) : OrderRepository {

    override suspend fun save(order: Order): Order {
        val entity = mapper.toEntity(order)
        val saved = jpaRepo.save(entity)
        return mapper.toDomain(saved)
    }

    override suspend fun findById(id: OrderId): Order? =
        jpaRepo.findById(id.value)
            .map { mapper.toDomain(it) }
            .orElse(null)
}

**الإخراج:

TEXT 📖 للعرض فقط

▶ مثال: Application Layer

KOTLIN
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional
class OrderApplicationService(
    private val orderRepo: OrderRepository,
    private val paymentService: PaymentPort,
    private val eventPublisher: EventPublisherPort
) {

    suspend fun placeOrder(command: PlaceOrderCommand): Order {
        // 1. تحقق من صحة المدخلات
        require(command.items.isNotEmpty()) { "Order must have items" }

        // 2. إنشاء Aggregate
        val order = Order(
            id = OrderId(generateId()),
            customerId = CustomerId(command.customerId),
            items = command.items.map { it.toOrderItem() },
            status = OrderStatus.PENDING
        )

        // 3. معالجة الدفع
        val paymentResult = paymentService.processPayment(order.total())

        // 4. حفظ
        val saved = orderRepo.save(order)

        // 5. نشر حدث
        eventPublisher.publish(OrderEvent.Created(saved))

        return saved
    }

    suspend fun confirmOrder(orderId: OrderId): Order {
        val order = orderRepo.findById(orderId)
            ?: throw OrderNotFoundException(orderId)

        if (order.status != OrderStatus.PENDING) {
            throw InvalidOrderStateException("Cannot confirm ${order.status} order")
        }

        val confirmed = order.copy(status = OrderStatus.CONFIRMED)
        val saved = orderRepo.save(confirmed)

        eventPublisher.publish(OrderEvent.Confirmed(saved.id))
        return saved
    }
}

// Ports (واجهات) - في طبقة التطبيق
interface PaymentPort {
    suspend fun processPayment(amount: Long): PaymentResult
}

interface EventPublisherPort {
    suspend fun publish(event: OrderEvent)
}

data class PaymentResult(val success: Boolean, val transactionId: String?)

**الإخراج:

TEXT 📖 للعرض فقط

▶ مثال: Presentation Layer (Controllers)

KOTLIN
import org.springframework.web.bind.annotation.*

data class PlaceOrderRequestDto(
    val customerId: String,
    val items: List<OrderItemDto>
) {
    data class OrderItemDto(
        val productId: String,
        val quantity: Int,
        val unitPrice: Long
    )

    fun toCommand() = PlaceOrderCommand(
        customerId = customerId,
        items = items.map { OrderItem(it.productId, it.quantity, it.unitPrice) }
    )
}

data class OrderResponseDto(
    val id: String,
    val customerId: String,
    val status: String,
    val total: Long
) {
    companion object {
        fun fromDomain(order: Order) = OrderResponseDto(
            id = order.id.value,
            customerId = order.customerId.value,
            status = order.status.name,
            total = order.total()
        )
    }
}

@RestController
@RequestMapping("/api/orders")
class OrderController(private val service: OrderApplicationService) {

    @PostMapping
    suspend fun create(@Valid @RequestBody request: PlaceOrderRequestDto): OrderResponseDto {
        val order = service.placeOrder(request.toCommand())
        return OrderResponseDto.fromDomain(order)
    }

    @GetMapping("/{id}")
    suspend fun findOne(@PathVariable id: String): OrderResponseDto {
        val order = service.findById(OrderId(id))
            ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
        return OrderResponseDto.fromDomain(order)
    }

    @GetMapping
    suspend fun list(): List<OrderResponseDto> =
        service.findAll().map { OrderResponseDto.fromDomain(it) }

    @PostMapping("/{id}/confirm")
    suspend fun confirm(@PathVariable id: String): OrderResponseDto {
        val confirmed = service.confirmOrder(OrderId(id))
        return OrderResponseDto.fromDomain(confirmed)
    }
}

**الإخراج:

TEXT 📖 للعرض فقط

▶ مثال: Testing طبقات بشكل منفصل

KOTLIN
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*

// اختبار Domain (بدون Spring، JUnit خالص)
class OrderDomainTest {

    @Test
    fun `order can be confirmed from PENDING status`() {
        val order = Order(
            id = OrderId("ORD-001"),
            customerId = CustomerId("C001"),
            items = listOf(OrderItem("P1", 2, 100)),
            status = OrderStatus.PENDING
        )

        val confirmed = order.copy(status = OrderStatus.CONFIRMED)

        assertEquals(OrderStatus.CONFIRMED, confirmed.status)
    }

    @Test
    fun `order total is sum of item subtotals`() {
        val order = Order(
            id = OrderId("ORD-001"),
            customerId = CustomerId("C001"),
            items = listOf(
                OrderItem("P1", 2, 100),  // 200
                OrderItem("P2", 1, 50)    // 50
            ),
            status = OrderStatus.PENDING
        )

        assertEquals(250, order.total())
    }
}

// اختبار Application Service (مع Mockito)
class OrderApplicationServiceTest {
    private val repo = mockk<OrderRepository>()
    private val payment = mockk<PaymentPort>()
    private val events = mockk<EventPublisherPort>()
    private lateinit var service: OrderApplicationService

    @BeforeEach
    fun setUp() {
        service = OrderApplicationService(repo, payment, events)
    }

    @Test
    suspend fun `placeOrder saves and publishes event`() {
        val cmd = PlaceOrderCommand("C001", listOf(...))
        val order = Order(...)

        every { repo.save(any()) } returns order
        coEvery { payment.processPayment(any()) } returns PaymentResult(true, "TX-001")
        coEvery { events.publish(any()) } returns Unit

        val result = service.placeOrder(cmd)

        coVerify { repo.save(any()) }
        coVerify { events.publish(any()) }
        assertEquals(order, result)
    }
}

**الإخراج:

TEXT 📖 للعرض فقط

▶ مثال: Dependency Injection Configuration

KOTLIN
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class AppConfig {

    // Infrastructure beans
    @Bean
    fun orderRepository(jpaRepo: OrderJpaRepository, mapper: OrderMapper): OrderRepository =
        JpaOrderRepository(jpaRepo, mapper)

    @Bean
    fun paymentPort(httpClient: HttpClient): PaymentPort =
        HttpPaymentAdapter(httpClient)

    @Bean
    fun eventPublisher(kafkaTemplate: KafkaTemplate<String, Any>): EventPublisherPort =
        KafkaEventPublisher(kafkaTemplate)

    // Application service (الذي يعتمد على Ports)
    @Bean
    fun orderApplicationService(
        orderRepo: OrderRepository,
        payment: PaymentPort,
        events: EventPublisherPort
    ): OrderApplicationService = OrderApplicationService(orderRepo, payment, events)

    // HTTP Client for external API
    @Bean
    fun httpClient(): HttpClient = HttpClient {
        install(ContentNegotiation) {
            json()
        }
        install(HttpTimeout) {
            requestTimeoutMillis = 30_000
        }
    }
}

**الإخراج:

TEXT 📖 للعرض فقط

▶ مثال: Integration Testing

KOTLIN
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ActiveProfiles

@SpringBootTest
@ActiveProfiles("test")
class OrderIntegrationTest {
    @Autowired
    lateinit var controller: OrderController

    @Autowired
    lateinit var repo: OrderRepository

    @Test
    suspend fun `full order lifecycle through API`() {
        // Place order
        val placed = controller.create(PlaceOrderRequestDto(
            customerId = "C001",
            items = listOf(PlaceOrderRequestDto.OrderItemDto("P1", 2, 100))
        ))

        assertEquals("PENDING", placed.status)

        // Confirm order
        val confirmed = controller.confirm(placed.id)
        assertEquals("CONFIRMED", confirmed.status)

        // Find in repository
        val fromRepo = repo.findById(OrderId(placed.id))
        assertNotNull(fromRepo)
        assertEquals(OrderStatus.CONFIRMED, fromRepo!!.status)
    }
}

**الإخراج:

TEXT 📖 للعرض فقط

❓ أسئلة شائعة

س هل يجب أن تعتمد طبقة النطاق على إطار عمل؟
ج لا. طبقة النطاق هي كود كوتلن بحت بدون تبعية على Spring أو أي إطار عمل. هذا يضمن أن منطق النطاق يمكن اختباره وإعادة استخدامه بشكل مستقل.
س هل يجب أن تُرجع طبقة الخدمة كائنات النطاق أم DTOs؟
ج الخدمة تُرجع كائنات النطاق؛ وحدة التحكم تحولها إلى DTOs. مسؤولية طبقية واضحة: الخدمة ليس لديها وعي بـ HTTP.
س أين يجب أن تعيش آلة الحالة؟
ج في طبقة النطاق. آلة الحالة هي منطق أعمال أساسي ولا يجب أن تعتمد على أي طبقة خارجية. نفّذها بفئات sealed بحتة في كوتلن.
س كيف تتعامل مع تحديثات الحالة المتزامنة؟
ج القفل المتفائل في قاعدة البيانات (حقل إصدار) أو أقفال موزعة. على مستوى الكوروتينات، استخدم Mutex لحماية الحالة المشتركة. للإنتاج، يُوصى بالقفل على مستوى قاعدة البيانات.
س أي عميل HTTP يجب أن تستخدمه طبقة التكامل؟
ج لمشاريع كوتلن، يُوصى بعميل Ktor (أصلي للكوروتينات). في أنظمة Java البيئية، Spring WebClient يعمل أيضًا. كلاهما يدعم suspend.
س كيف تضمن تقسيم الفريق للعمل حسب الطبقات؟
ج قواعد مراجعة الكود: وحدات التحكم لا تلمس المستودع مباشرة، الخدمات لا تعرف عن HTTP، طبقة النطاق لا تعتمد على أطر العمل. أدوات مثل ArchUnit يمكنها فرض ذلك تلقائيًا.

📖 ملخص


📝 تمارين

  1. مبتدئ (⭐): نفّذ فئة OrderItem و OrderStatus sealed class مع التحقق في كتلة init. تلميح: require()
  2. متوسط (⭐⭐): نفّذ دوال confirm() و ship() في OrderService باستخدام آلة الحالة للتحقق من الانتقالات. تلميح: StateMachine.transition()
  3. متقدم (⭐⭐⭐): نفّذ OrderProcessor كامل بأربع طبقات: نطاق (Order + Event + StateMachine)، خدمة (OrderService + كوروتينات)، استمرارية (InMemoryRepo)، تكامل (MockPaymentClient). تلميح: راجع المثال الكامل في القسم 9

← السابق | التالي →

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%