Modelarea Domeniului de Afaceri cu Programare Funcțională Tipizată în Kotlin: Biblioteca Arrow (Either și Raise DSL) în Kotlin

Erori ca valori (Result, runCatching) Arrow 1.2+ / Kotlin 2.0+

Ghid avansat de programare funcțională și modelare tipizată a domeniului în Kotlin cu Arrow: eliminarea excepțiilor prin Either, noul DSL Raise<E> de mare performanță bazat pe continuări, acumularea validărilor cu NonEmptyList și optici (Lenses).

Sintaxă
either<DomainError, Order> { val user = findUser(id).bind(); ... }

În sistemele software complexe de tip bancar, logistic sau telecomunicații, codul care utilizează excepții runtime (throw IllegalStateException(...)) suferă de defecte majore de design: semnăturile de funcții mint dezvoltatorul (spun că returnează un User, dar ascund faptul că pot crăpa oricând), compilatorul nu poate garanta că toate erorile sunt tratate, iar crearea de instanțe Throwable umple inutil stiva de apel (stack trace overhead), degradând sever performanța.

Arrow-kt este biblioteca de referință pentru Programare Funcțională (FP) în limbajul Kotlin. Odată cu Arrow 1.2+ și compatibilitatea deplină Kotlin 2.0, Arrow a introdus noul DSL Raise<E>: un model revoluționar bazat pe continuări care oferă sintaxa intuitivă a excepțiilor (cu oprire la prima eroare), dar cu garanția deplină de tip la compilare și performanță identică cu salturile locale de cod!

Mecanismul Raise<E> vs Excepții Tradiționale
# 1. Modelul Bazat pe Excepții Tradiționale:
#    fun process(order: Order): ResultOrder {
#        throw PaymentException()  ---> Umple stack trace, rupe firul de execuție,
#    }                             ---> Semnătura nu declară PaymentException!
#                                  ---> Tratat doar prin try/catch la runtime.

# 2. Modelul Funcțional Arrow Raise DSL:
#    fun Raise.process(order: Order): ResultOrder {
#        raise(OrderError.PaymentFailed) ---> Oprește execuția local fără alocare pe heap!
#    }                                   ---> Tipul erorii este obligatoriu în semnătură!
#                                        ---> Compilatorul forțează tratarea completă.

Tabel Comparativ: Metode de Tratare a Erorilor în Kotlin

Tehnică Utilizată Siguranță la Compilare Overhead de Memorie / CPU Acumulare Erori Multiple Ergonomie Sintactică
throw Exception Zero (Excepții neverificate pe JVM) Foarte mare (construire completă a stivei de apel) Nu (se oprește la prima excepție) Cod imperativ clasic try/catch
kotlin.Result<T> Parțială (Eroarea este întotdeauna doar Throwable) Redus (Value class pe JVM) Nu nativ Metode de tip mapCatching, getOrElse
Arrow Either<E, A> Totală (Tipul E este specificat exact) Mediu (Alocare obiecte Left și Right) Da (prin zipOrAccumulate) Blocuri either { ... } și bind()
Arrow Raise<E> DSL Totală (Eroarea E este receptor de context) Zero-Cost (Fără alocare pe heap, optimizat de K2) Da (prin zipOrAccumulate cu NonEmptyList) Sintaxă directă imperativă raise(eroare)

Implementare de Nivel Enterprise: Pipeline de Validare și Procesare Comandă

Exemplul de mai jos demonstrează modelarea domeniului cu Sealed Interfaces, validarea paralelă cu acumulare de erori multiple (NonEmptyList) și execuția tranzacțională a unei comenzi:

OrderDomainService.kt – Modelare Funcțională cu Arrow 1.2+
package ro.dexblog.arrow.domain

import arrow.core.Either
import arrow.core.NonEmptyList
import arrow.core.nonEmptyListOf
import arrow.core.raise.Raise
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.raise.zipOrAccumulate

// 1. Ierarhie strictă de erori de domeniu (Domain Errors)
sealed interface OrderError {
    data class ValidationFailed(val errors: NonEmptyList) : OrderError
    data class CustomerNotFound(val customerId: String) : OrderError
    data class InsufficientFunds(val available: Double, val required: Double) : OrderError
    data class InventoryUnavailable(val unavailableItems: List) : OrderError
}

data class OrderItem(val sku: String, val quantity: Int, val unitPrice: Double)
data class RawOrderDto(val customerId: String, val items: List)
data class ValidatedOrder(val customerId: String, val items: List, val total: Double)
data class CompletedOrder(val orderId: String, val totalAmount: Double, val status: String)

class OrderProcessingService {

    // Validare primară cu acumularea tuturor erorilor simultan!
    private fun Raise.validate(dto: RawOrderDto): ValidatedOrder {
        zipOrAccumulate(
            { ensure(dto.customerId.isNotBlank()) { "ID-ul clientului nu poate fi gol!" } },
            { ensure(dto.items.isNotEmpty()) { "Comanda trebuie să aibă cel puțin o linie!" } },
            {
                dto.items.forEach { item ->
                    ensure(item.quantity > 0) { "Cantitatea pentru produsul ${item.sku} trebuie să fie pozitivă!" }
                    ensure(item.unitPrice > 0.0) { "Prețul pentru produsul ${item.sku} trebuie să fie valid!" }
                }
            }
        ) { _, _, _ ->
            val total = dto.items.sumOf { it.quantity * it.unitPrice }
            ValidatedOrder(dto.customerId, dto.items, total)
        }
    }

    // Funcție de domeniu cu receptorul de context Raise
    private fun Raise.chargeCustomer(customerId: String, amount: Double) {
        val customerBalance = 150.0 // Simulare interogare cont
        if (customerBalance < amount) {
            raise(OrderError.InsufficientFunds(available = customerBalance, required = amount))
        }
    }

    // Orchestrator tranzacțional complet expus ca funcție pură returnând Either
    fun processOrder(dto: RawOrderDto): Either = either {
        // Pasul 1: Validare date de intrare (Erorile sunt acumulate)
        val validOrder = validate(dto)

        // Pasul 2: Verificare sold client
        chargeCustomer(validOrder.customerId, validOrder.total)

        // Pasul 3: Rezervare stoc și finalizare
        CompletedOrder(
            orderId = "ORD-" + System.currentTimeMillis(),
            totalAmount = validOrder.total,
            status = "CONFIRMED"
        )
    }
}

fun main() {
    val service = OrderProcessingService()

    // Test 1: Comandă invalidă (acumulare erori multiple)
    val invalidDto = RawOrderDto(customerId = "", items = emptyList())
    val resultInvalid = service.processOrder(invalidDto)
    println("Rezultat comandă invalidă:\n$resultInvalid\n")

    // Test 2: Comandă cu fonduri insuficiente
    val validDto = RawOrderDto(
        customerId = "CUST-102",
        items = listOf(OrderItem("SKU-LAPTOP", 1, 1200.0))
    )
    val resultFunds = service.processOrder(validDto)
    println("Rezultat fonduri insuficiente:\n$resultFunds")
}