Kotlin: شرح تطوير مشاريع كوتلن
آخر تحديث: 2026-08-26
التصميم انتهى - الآن وقت الكود. يتعاون Charlie و Alice و Bob لتنفيذ كود الطبقات الأربع لـ OrderProcessor: طبقة النطاق (طلب + أحداث)، طبقة الخدمة (منطق الأعمال + كوروتينات)، طبقة الاستمرارية (مستودع)، وطبقة التكامل (عملاء HTTP).
1. ما ستتعلمه
- طبقة النطاق:
data class/sealed class/StateMachine - طبقة الخدمة:
OrderService+ معالجة غير متزامنة + تدفق أحداثFlow - طبقة الاستمرارية: Spring Data R2DBC + مستودع الكوروتينات
- طبقة التكامل: عميل Ktor HTTP لاستدعاءات الخدمات الخارجية
- سير عمل التعاون بين Alice/Bob/Charlie
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. تسلسل إنشاء طلب كامل
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 يمكنها فرض ذلك تلقائيًا.
📖 ملخص
- طبقة النطاق: كوتلن بحت، فئة data + فئة sealed + آلة حالة، بدون تبعية على إطار عمل
- طبقة الخدمة: منطق الأعمال + انتقالات الحالة + نشر الأحداث، تستدعي طبقة المستودع والتكامل
- طبقة الاستمرارية: تجريد واجهة + تنفيذ في الذاكرة (تطوير) + تنفيذ قاعدة بيانات (إنتاج)
- طبقة التكامل: واجهات عميل HTTP + تنفيذات محاكاة (تطوير) + تنفيذات Ktor/Spring (إنتاج)
- تقسيم العمل الطبقي: Charlie يتولى النطاق، Alice تتولى الخدمة، Bob يتولى الاستمرارية/التكامل
- آلة الحالة + مصدر الأحداث: تسجيل الأحداث لكل تغيير حالة، دعم مسار تدقيق كامل
📝 تمارين
- مبتدئ (⭐): نفّذ فئة
OrderItemوOrderStatussealed class مع التحقق في كتلةinit. تلميح:require() - متوسط (⭐⭐): نفّذ دوال
confirm()وship()فيOrderServiceباستخدام آلة الحالة للتحقق من الانتقالات. تلميح:StateMachine.transition() - متقدم (⭐⭐⭐): نفّذ OrderProcessor كامل بأربع طبقات: نطاق (Order + Event + StateMachine)، خدمة (OrderService + كوروتينات)، استمرارية (InMemoryRepo)، تكامل (MockPaymentClient). تلميح: راجع المثال الكامل في القسم 9