Optimisation du code Kotlin : Collections immuables et fonctions d'ordre supérieur

L'un des principes fondamentaux pour écrire un code robuste en Kotlin consiste à privilégier l'immuabilité. En utilisant systématiquement val au lieu de var, on s'assure que les références ne changent pas, ce qui réduit considérablement les effets de bord et facilite le débogage, notamment dans les environnements multi-threadés.

Privilégier les collections immuables

Alors que Java a mis du temps à proposer des solutions natives simples pour les collections non modifiables (via List.of() en Java 9+), Kotlin intègre cette philosophie dès sa conception. Le langage distingue clairement les interfaces de lecture seule des interfaces modifiables.

// Création de structures de données en lecture seule
val nombres = listOf(10, 20, 30)
val dictionnaire = mapOf("A" to 1, "B" to 2)
val categories = setOf("Admin", "User", "Guest")

Cette approche permet d'utiliser la puissance de la programmation fonctionnelle en chaînant les opérations sans altérer la source de données originale :

val resultatsFiltrés = listOf(10, 20, 30, 40)
    .map { it / 10 }
    .filter { it > 2 } 
// Retourne [3, 4] sans modifier la liste initiale

Dans certains cas, la mutation est nécessaire. Kotlin permet alors d'être explicite en utilisant des fonctions dédiées comme mutableListOf().

fun gestionStock() {
    // Liste immuable par défaut
    val catalogueFixe = listOf("Laptop", "Souris")
    // catalogueFixe.add("Clavier") // Erreur de compilation

    // Liste modifiable si nécessaire
    val inventaireDynamique = mutableListOf("Écran")
    inventaireDynamique.add("Clavier") // Autorisé
}

Prenons un exemple concret : agréger des données de ventes. Plutôt que de manipuler manuellement une Map mutable au sein d'une boucle, l'approche fonctionnelle utilisant les collections immuables est souvent plus lisible et moins sujette aux erreurs.

data class Transaction(val reference: String, val mois: String, val montant: Double)

fun main() {
    val historique = listOf(
        Transaction("T01", "Janvier", 120.0),
        Transaction("T02", "Février", 200.0),
        Transaction("T03", "Janvier", 180.0),
        Transaction("T04", "Mars", 350.0)
    )

    // Approche déclarative avec groupBy et mapValues
    val revenusParMois = historique
        .groupBy { it.mois }
        .mapValues { (_, transactions) -> 
            transactions.sumOf { it.montant } 
        }

    println(revenusParMois) // {Janvier=300.0, Février=200.0, Mars=350.0}
}

Exploiter les fonctions d'ordre supérieur

Une fonction d'ordre supérieur est une fonction qui accepte une autre fonction comme paramètre ou qui en retourne une. Ce concept, issu de la programmation fonctionnelle, permet d'abstraire des comportements et de rendre le code extrêmement modulaire.

Voici un exemple simple où une fonction calculer délègue la logique mathématique à un paramètre nommé strategie :

fun executerCalcul(x: Int, y: Int, strategie: (Int, Int) -> Int): Int {
    return strategie(x, y)
}

fun main() {
    val addition = { a: Int, b: Int -> a + b }
    val soustraction = { a: Int, b: Int -> a - b }

    println(executerCalcul(10, 5, addition))     // 15
    println(executerCalcul(10, 5, soustraction))  // 5
}

Les fonctions d'ordre supérieur sont particulièrement utiles pour encapsuler des logiques transverses, comme la mesure de performance ou la gestion des logs. Imaginons un utilitaire pour chronométrer l'exécution d'un bloc de code :

fun <T> mesurerPerformance(nomTache: String, bloc: () -> T): T {
    val debut = System.currentTimeMillis()
    val resultat = bloc()
    val fin = System.currentTimeMillis()
    println("[$nomTache] Exécuté en ${fin - debut} ms")
    return resultat
}

fun traitementLourd(): String {
    Thread.sleep(1500)
    return "Données traitées"
}

fun main() {
    val retour = mesurerPerformance("Analyse Data") {
        traitementLourd()
    }
    println("Résultat final : $retour")
}

Dans cet exemple, mesurerPerformance prend en charge la gestion du temps, tandis que le code métier reste isolé dans la fonction traitementLourd. Cette séparation des préoccupations rend l'architecture logicielle plus propre et facilite la réutilisation du code utilitaire sur n'importe quel segment du projet.

Étiquettes: kotlin Functional Programming Immutability Software Design collections

Publié le 22 juillet à 12h50