1. Introduction
Un mini-API est un petit ensemble de fonctions, d’extensions et (parfois) d’operateurs qui rapproche le code du domaine metier. Pas « plus proche de Kotlin », pas « plus proche des DSL a la mode », mais plus proche de ce que vous faites vraiment : ajouter des depenses, calculer un total, construire un rapport, formatter une sortie. C’est une couche qui evite les repetitions et rend main semblable a un scenario plutot qu’a un depotoir de details.
Il est tres facile de confondre un mini-API avec « ecrire son propre langage de programmation en Kotlin ». Mais l’objectif est bien plus modeste : permettre a la personne suivante (et a vous dans deux semaines) d’ouvrir le fichier et de comprendre ce qui se passe, sans avoir a deviner si « l’operateur plus additionne des nombres ou lance une fusee ». C’est pourquoi nous allons constamment nous poser la question : en lisant la ligne de code, est-ce qu’on comprend ce qui va se passer ?
Imaginons que nous continuions une application console d’apprentissage pour le suivi des depenses (un ExpenseTracker hypothetique). Avant, nous avions probablement une liste MutableList<Expense>, des commandes add/list/remove, et quelques rapports. Maintenant, nous voulons que les rapports se construisent de facon lisible et uniforme — et c’est precisement la que le mini-API est pertinent.
Trois regles : « peu couteux », « sans surprises », « le nom rend le sens evident »
Quand vous commencez a ajouter des extensions et des operateurs, la tentation apparait de « cacher » les details si profondement que le code devient joli, mais cesse d’etre honnete. Les conventions Kotlin donnent un bon repere : si quelque chose ressemble a une propriete, cela doit etre peu couteux et sans surprises ; si l’action est significative, il vaut mieux que ce soit une fonction. Cette regle est particulierement utile ici, car un mini-API est presque toujours construit sur des « petites choses » appelees tres souvent.
Mettons ces trois regles sous forme de check-list de comportement :
| Outil | Quand c’est approprie | Attentes du lecteur | « Mauvaises surprises » typiques |
|---|---|---|---|
| Extension-fonction fun X.foo() | Etape de traitement repetee, formatage, petite operation metier | C’est une action, on peut la lire comme un verbe | foo fait trop de choses, touche a l’I/O, mute tout et n’importe quoi |
| Extension-propriete val X.bar | Valeur derivee, verification legere | C’est « comme un champ » : rapide, stable, sans effets de bord | get() trie une liste, ecrit dans la console ou appelle le reseau |
| Operateur operator fun plus/compareTo/get/set | Sens communement admis : addition, comparaison, indexation | Le symbole doit se lire sans ambiguite | + mute l’objet, [] fait « ajouter si absent », compareTo compare « au hasard » |
| infix fun a foo b | Rarement et de facon ponctuelle : deux roles « presque egaux » (comme to) | Ressemble a une mini-phrase | L’utiliser pour la mutation ou la ou une fonction normale est plus claire |
A propos de infix, il est utile de se souvenir des contraintes formelles : une fonction infix doit avoir exactement un parametre, sans vararg et sans valeur par defaut. Les appels infix ont aussi leur propre priorite ; des expressions sans parentheses se lisent parfois differemment de leur execution.
2. Mini-modele et rapport
Modele : Expense et Money
Pour que les exemples ne soient pas eparpilles, mettons-nous d’accord sur de petits types autour desquels nous allons construire le mini-API. Nous n’entrons pas dans l’architecture et les couches — nous gardons simplement le code propre et centre sur le domaine.
Commencons par l’argent. D’apres les sujets precedents, nous connaissons deja value class ; mettons donc l’argent en centimes, pour eviter les problemes de Double en finance :
@JvmInline
value class Money(val cents: Long)
Et une depense :
data class Expense(
val title: String,
val amount: Money,
val category: String
)
Ici, la categorie est une chaine — volontairement. Oui, nous avons deja enum, mais aujourd’hui le sujet n’est pas le modele, c’est le mini-API au-dessus de l’affichage. On pourra ameliorer les categories a part, et notre couche de confort doit survivre aussi bien a une chaine qu’a un enum.
Decidons maintenant comment construire le rapport. L’option la plus simple est d’accumuler des chaines puis d’appeler joinToString("\n"). Mais nous voulons que l’ajout d’une ligne ait la meme forme partout, et que le formatage soit uniforme. Pour cela, creons une petite classe accumulateur Report.
Report comme accumulateur : quand += est approprie
L’operateur += est souvent percu comme « c’est forcement une modification ». Et c’est vrai : selon les regles des conventions d’operateurs, a += b devient a.plusAssign(b), si une telle methode existe, et le compilateur attend un resultat Unit. C’est parfait pour un accumulateur : il existe pour qu’on lui ajoute des choses.
Creons un Report qui accumule des lignes :
class Report {
private val lines = mutableListOf<String>()
operator fun plusAssign(line: String) {
lines.add(line)
}
override fun toString(): String = lines.joinToString("\n")
}
Point important de previsibilite : Report est un objet-conteneur mutable. Donc += signifie bien « ajoute a l’interieur ». C’est similaire a += sur MutableList : pour les collections mutables, il ajoute des elements « sur place ».
Verifions avec un mini-exemple :
fun main() {
val r = Report()
r += "TOTAL: 350"
r += "TOP: food"
println(r)
// TOTAL: 350
// TOP: food
}
Si nous faisions en sorte que r + "..." mute r, ce serait moins bien : + se lit generalement comme « creer une nouvelle valeur » (comme pour List + element, qui renvoie une nouvelle collection). La regle est donc simple : mutation → plusAssign ou une methode explicite ; creation d’un nouveau resultat → plus.
Extensions pour Report : moins de copier-coller, plus d’uniformite
Quand Report apparait, la tentation suivante est d’ecrire partout des chaines a la main : "TOTAL: ${...}", "COUNT: ${...}" etc. Le probleme n’est pas que ce soit difficile, mais que cela devient vite « chacun fait comme il veut » : ici deux-points, la un tiret, ici des espaces, la des casses differentes. Un mini-API sert a rendre le style uniforme et reproductible.
Ajoutons une extension-fonction qui ecrit une paire cle–valeur avec un style unique :
fun Report.kv(key: String, value: Any?) {
this += "$key: $value"
}
Notez que nous ne faisons volontairement ni infix, ni operateur. C’est une action ordinaire « ajouter une ligne », et le nom kv (key-value) est court, mais reste lisible dans le contexte d’un rapport.
Utilisation :
fun main() {
val r = Report()
r.kv("TOTAL", 350)
r.kv("COUNT", 12)
println(r)
// TOTAL: 350
// COUNT: 12
}
Ajoutons maintenant des fonctions un peu plus « metier ». Par exemple, l’affichage de l’argent : nous ne voulons pas nous souvenir des centimes a chaque fois.
fun Money.format(): String = "%.2f".format(cents / 100.0)
Et utilisons cela dans Report :
fun Report.money(key: String, value: Money) {
this += "$key: ${value.format()}"
}
Desormais, l’affichage de l’argent est stable, et main ne contient pas d’arithmetique « en bonus ».
3. Formatage et syntaxe pratique
Mini-API pour StringBuilder : chainee sans magie
Parfois, il est plus pratique de construire un rapport sans classe dediee, directement sous forme de chaine. Pour cela, Kotlin utilise souvent StringBuilder et l’approche « construire → renvoyer la chaine ». Pour de grandes chaines, StringBuilder est plus efficace que la concatenation dans une boucle, et c’est un bon moment pour y ajouter un mini-API.
Creons une extension-fonction qui ajoute une ligne avec retour a la ligne et renvoie this pour permettre le chainage :
fun StringBuilder.line(text: String): StringBuilder =
append(text).append('\n')
La construction ressemble maintenant a un « flux de lignes » :
fun main() {
val text = StringBuilder()
.line("Expense report")
.line("--------------")
.line("TOTAL: 350")
.toString()
print(text)
// Expense report
// --------------
// TOTAL: 350
}
Pourquoi est-ce un bon mini-API et pas un « exces » ? Parce que le sens est evident : line("...") ajoute une ligne. Il n’y a pas de logique cachee, pas d’effets de bord, pas de mutation inattendue en dehors de StringBuilder. La chaine se lit de gauche a droite comme un scenario, tout en restant honnete.
infix avec parcimonie : quand c’est vraiment lisible
infix est joli, mais traitre, car on a envie de l’utiliser « partout ». Les conventions conseillent de declarer infix uniquement quand les deux cotes ont des roles « similaires », comme dans to ou zip, et d’eviter infix pour la mutation.
Il est parfois pratique d’ecrire des paires « cle–valeur », surtout si l’on veut ensuite les convertir en Map. Par exemple, dans des rapports, on peut d’abord rassembler les donnees, puis afficher.
Creons un infix eq qui construit un Pair :
infix fun String.eq(value: Any?): Pair<String, Any?> =
this to value
Utilisation :
fun main() {
val p = "TOTAL" eq 350
println(p) // (TOTAL, 350)
}
Ici, infix est justifie : la chaine a gauche est une « cle », la valeur a droite est une « valeur », les roles sont assez symetriques. En plus, il ne s’agit pas d’une mutation, mais de la creation d’une valeur.
Bonus pratique : a partir d’un tableau/une liste de Pair, on peut construire un Map via toMap(), mais il faut se rappeler que les cles doivent etre uniques (la derniere ecrase la premiere).
Mini-API pour ExpenseTracker : un rapport a partir d’une liste de depenses
Rassemblons maintenant tout cela en une petite histoire : nous avons une liste de depenses, et nous voulons construire un court rapport texte. Pas un « mega-systeme », mais une fonction propre, appelable depuis main.
Creons quelques petites extension-fonctions sur la liste des depenses. Oui, c’est un « API au-dessus des collections », et Kotlin le fait activement : une enorme partie de la bibliotheque standard est composee d’extension-fonctions.
Calculons le total de toutes les depenses :
fun List<Expense>.total(): Money {
var sum = 0L
for (e in this) sum += e.amount.cents
return Money(sum)
}
Et construisons un rapport :
fun List<Expense>.toReport(): Report {
val r = Report()
r += "Expense report"
r.money("TOTAL", this.total())
r.kv("COUNT", this.size)
return r
}
Testons :
fun main() {
val items = listOf(
Expense("Coffee", Money(350), "food"),
Expense("Taxi", Money(1200), "transport")
)
println(items.toReport())
// Expense report
// TOTAL: 15.50
// COUNT: 2
}
Pourquoi est-ce une bonne couche de mini-API ?
- Premiere raison : main ne contient presque aucun detail ; il dit « j’ai des depenses, fais un rapport ».
- Deuxieme raison : toReport() est une fonction parlante, et son nom indique clairement qu’elle renvoie un objet Report.
- Troisieme raison : le mini-API est petit : total(), toReport(), Report.kv(), Report.money(), Money.format().
Ce n’est pas une « bibliotheque universelle pour tout », mais des commodites ciblees pour le besoin.
4. Zones a risque et choix de l’outil
Quand le mini-API devient dangereux
Des que vous y prendrez gout, une envie apparaitra : « et si on faisait en sorte que toute action ressemble a un bouton unique ». Et la, Kotlin commence a tester votre maturite de developpeur.
Par exemple, une mauvaise idee est de faire une extension-propriete val List<Expense>.report: String, qui, en interne, construit un rapport, trie, groupe et formate. Cela ressemble a un champ, mais fait beaucoup de travail. Les conventions recommandent : une propriete est appropriee si le calcul est peu couteux, ne lance pas d’exception et est stable pour un meme etat. Si votre rapport peut etre couteux, cela doit etre une fonction (fun buildReport()), afin que le lecteur s’attende a « executer du travail ».
Autre point subtil : les operateurs sur les collections. Beaucoup ont vu list + element et pensent que cela signifie « ajoute a la liste ». Mais pour les collections en lecture seule, + renvoie une nouvelle liste, et += pour une variable var peut etre une reassignment du resultat, alors que pour MutableList c’est une mutation sur place. Ce n’est pas un probleme si vous le comprenez, mais dans un mini-API il vaut mieux ne pas faire deviner le lecteur. C’est pourquoi nous avons fait de Report un accumulateur avec plusAssign, ou le sens est sans ambiguite.
Et enfin, infix. Oui, a foo b est joli. Mais infix a une priorite, et des expressions sans parentheses se lisent parfois differemment de leur execution. Donc, gardons infix pour des operations « binaires » tres simples, et faisons le reste avec des fonctions ordinaires.
Comment choisir : extension / operator / infix
Pour ne pas garder tout cela en tete comme « cinquante regles », il est pratique d’avoir un petit algorithme de decision. Imaginez que vous vouliez ajouter une commodite, et posez-vous les questions dans cet ordre :
1) Est-ce que cela ressemble a une "lecture de propriete" ?
Oui → fais un val (ou un extension-val), mais seulement si c’est peu couteux et sans effets de bord.
Non → continue.
2) Est-ce une action avec un verbe clair ?
Oui → une fonction (normale ou extension).
Non → continue.
3) Existe-t-il un symbole communement admis pour cela (+, [], comparaison) ?
Oui → operator, mais seulement si le sens est sans ambiguite.
Non → continue.
4) Les deux cotes sont "presque egaux" et cela se lira comme une phrase ?
Oui → infix (rarement et avec precaution).
Non → une fonction ordinaire, et ne te torture pas.
Ce n’est pas une « loi Kotlin », mais une habitude pratique qui protege contre un code qui ressemble a de la magie. Et la magie en production est mauvaise parce qu’elle fonctionne exactement jusqu’au premier bug — ensuite, vous decouvrez soudain que la « baguette magique » etait en fait un baton a double tranchant.
5. Erreurs typiques
Erreur n°1 : « Je vais faire un operateur, parce que je peux ».
Les operateurs ne servent pas a la beaute, mais a la previsibilite. Si a + b ne se lit pas comme « additionner », ou si obj[i] ne se lit pas comme « obtenir par index/cle », vous rendez le code pire. Dans ces cas, une methode ordinaire (addExpense, appendLine) gagne presque toujours.
Erreur n°2 : Cacher un travail lourd dans une extension-propriete.
Une propriete ressemble a un acces rapide aux donnees ; le lecteur ne s’attend donc pas a des tris, regroupements, construction de rapport, et encore moins a des exceptions. Les conventions recommandent de n’utiliser une propriete que pour des calculs peu couteux et stables. Si le travail est notable, faites une fonction.
Erreur n°3 : Ne pas distinguer + et += par le sens « nouvelle valeur » vs « mutation ».
En Kotlin, += est compile selon des regles specifiques : soit via plusAssign, soit par « recreation » via a = a + b. Si vous surchargez des operateurs dans votre type, gardez une semantique parfaitement claire : plus cree generalement une nouvelle valeur, plusAssign modifie l’existante.
Erreur n°4 : Faire un infix pour la mutation ou pour des actions aux roles differents.
infix a fiere allure la ou les deux cotes sont logiquement symetriques (comme cle–valeur ou union d’ensembles), et se comporte mal la ou c’est une commande ordinaire « fais X ». Les conventions conseillent explicitement de ne pas faire de methodes infix qui mutent le receveur.
Erreur n°5 : Gonfler le mini-API jusqu’a un « depotoir d’extensions ».
Si vous empilez toutes les extensions dans un seul fichier Extensions.kt de mille lignes, vous obtiendrez non pas un mini-API, mais un mini-chaos. Il vaut mieux garder les extensions thematiquement proches de ce qu’elles servent (par exemple, ReportExtensions.kt, MoneyFormatting.kt) et eviter de multiplier les memes noms a differents endroits, pour ne pas ensuite vous battre avec les imports.
Erreur n°6 : Faire un mini-API qu’on ne peut pas « lire a l’œil ».
La pire degradation de lisibilite arrive quand une ligne de code cesse d’etre autonome. Si, sans connaitre des conventions internes, on ne peut pas savoir si l’on mute un objet ou si l’on cree un nouveau resultat, si l’appel est lourd, s’il a des effets de bord — le mini-API n’aide pas, il gene. Une bonne couche de confort reste transparente : elle reduit le bruit, mais ne cache pas le sens.
GO TO FULL VERSION