Riadh Mnasri
← Retour au blog
4 min de lecture

Sealed classes : modéliser un ensemble fini de cas sans if/else en cascade

Dans l'article sur Kotlin, un tableau mentionne les sealed classes en une ligne, à côté des data classes et de la null-safety. Ce point mérite d'être détaillé seul : c'est l'outil le plus sous-utilisé du langage pour modéliser un résultat métier, et celui dont l'absence se paie le plus cher en production.

Le problème que résout une sealed class

Modéliser le résultat d'une validation métier avec un enum classique ou une chaîne de statuts fonctionne, jusqu'au jour où deux cas de l'enum transportent des données différentes : un rejet a besoin d'un motif, une validation en attente a besoin d'une date d'échéance, une acceptation n'a besoin de rien. La solution la plus courante, un objet avec des champs optionnels pour chaque cas, laisse des combinaisons incohérentes possibles (un statut « accepté » avec un motif de rejet rempli n'importe où dans le code) que le compilateur ne peut pas détecter.

sealed class LimitCheckResult {
    data object Approved : LimitCheckResult()
    data class Rejected(val reason: String) : LimitCheckResult()
    data class PendingReview(val reviewBy: LocalDate) : LimitCheckResult()
}

Chaque cas ne porte que les données qui le concernent : Approved n'a aucun champ, Rejected exige un motif, PendingReview exige une date. Aucune combinaison incohérente n'est représentable.

Ce que l'exhaustivité change vraiment

Le bénéfice concret ne vient pas de la déclaration, mais de l'usage : un when exhaustif sur une sealed class oblige à traiter chaque cas, et le compilateur refuse de compiler si un cas est oublié, y compris quand un nouveau cas est ajouté des mois plus tard.

fun describe(result: LimitCheckResult): String = when (result) {
    is LimitCheckResult.Approved -> "Validé"
    is LimitCheckResult.Rejected -> "Rejeté : ${result.reason}"
    is LimitCheckResult.PendingReview -> "En attente jusqu'au ${result.reviewBy}"
}

Si PendingReview est ajouté six mois après l'écriture de cette fonction, chaque when exhaustif existant dans la base de code refuse de compiler tant qu'il n'a pas été mis à jour. Avec un enum classique et un when non-exhaustif (ou pire, une cascade de if/else if), ce même oubli compile sans erreur, et le nouveau cas tombe silencieusement dans une branche else qui n'a pas été pensée pour lui.

ApprocheNouveau cas oublié dans un traitementDétection
Enum + when avec elseCompile, tombe dans la branche elseAucune, découvert en production
Enum + if/else if en cascadeCompile, tombe dans le dernier elseAucune, découvert en production
Sealed class + when exhaustifNe compile pasImmédiate, à la compilation

La limite : un ajout de cas devient un changement cassant pour les autres

L'exhaustivité du when repose sur le fait que le compilateur connaît tous les sous-types possibles d'une sealed class au moment de la compilation ; c'est précisément pour ça qu'une sealed class reste fermée à son module de déclaration, aucun module externe ne peut lui ajouter un sous-type. Ce qui mérite d'être anticipé, c'est la conséquence pour les consommateurs : si cette sealed class fait partie d'une librairie utilisée par d'autres équipes, ajouter un nouveau cas casse la compilation de tout when exhaustif existant chez eux, dès qu'ils recompilent contre la nouvelle version. C'est exactement le comportement recherché à l'intérieur d'une même équipe (rien ne doit être oublié), mais ça devient un changement d'API cassant, à documenter et versionner comme tel, dès que la sealed class franchit la frontière d'une librairie partagée.

Sealed class vs enum : quand utiliser lequel

SituationBon choix
Cas fixes sans donnée associée (jours de la semaine, statuts simples)enum class
Cas dont chacun porte des données différentessealed class ou sealed interface
Besoin de vérifier l'exhaustivité du traitement à la compilationsealed class avec when exhaustif
Sérialisation simple vers une base de données (une seule colonne)enum class, plus direct

Le critère qui tranche n'est pas la préférence stylistique, c'est la question « est-ce que chaque cas a besoin de données différentes, et est-ce que je veux que l'ajout d'un cas casse la compilation partout où il n'est pas géré ? ». Si la réponse aux deux est oui, la sealed class n'est pas une option parmi d'autres, c'est le seul outil du langage qui offre cette garantie.