Extension Function en Kotlin: Ajouter des Méthodes aux Classes
1. Aperçu
Lorsque vous travaillez avec des classes existantes comme String, List ou même des classes provenant de bibliothèques tierces, vous souhaitez souvent ajouter quelques fonctions utilitaires sans pouvoir modifier le code source original. Kotlin résout ce problème avec les extension function — un mécanisme qui permet « d’attacher » des fonctions à une classe sans avoir besoin d’héritage ou du pattern Decorator.
Cet article vous aide à comprendre comment fonctionnent les extension function, comment les appliquer à des situations concrètes dans un projet Spring Boot, et les pièges à éviter.
2. Qu’est-ce qu’une Extension Function ?
Une extension function est une fonction définie en dehors d’une classe mais qui peut être appelée comme si c’était une méthode de cette classe. La syntaxe générale :
fun ClassName.functionName(params): ReturnType {
// utiliser "this" pour référencer l'instance
}
Exemple simple : ajouter une fonction isValidEmail() à String.
fun String.isValidEmail(): Boolean {
return this.matches(Regex("^[\\w.+-]+@[\\w-]+\\.[a-zA-Z]{2,}$"))
}
fun main() {
val email = "user@example.com"
println(email.isValidEmail())
}
La ligne fun String.isValidEmail() déclare String comme receiver type. À l’intérieur de la fonction, this pointe vers la chaîne qui appelle la fonction. Le résultat affiché :
true
En substance, Kotlin compile les extension function en static method, recevant l’instance comme premier paramètre. Ainsi, les extension function ne modifient pas réellement la classe d’origine — ce n’est que du sucre syntaxique (syntactic sugar) pour rendre le code plus concis.
3. Application aux Domain Objects
Supposons que vous avez une classe Order dans un système de vente, vous voulez ajouter une fonction pour calculer le montant total après taxe sans vouloir mettre cette logique dans la classe d’origine (peut-être que la classe se trouve dans un autre module, ou vous voulez séparer la logique métier).
data class Order(
val id: Long,
val subtotal: Double,
val taxRate: Double
)
fun Order.totalWithTax(): Double {
return subtotal + (subtotal * taxRate)
}
fun main() {
val order = Order(id = 1, subtotal = 100.0, taxRate = 0.1)
println(order.totalWithTax())
}
La fonction totalWithTax() lit subtotal et taxRate via this (vous pouvez omettre this. car Kotlin l’infère automatiquement). Le résultat :
110.0
Cette approche aide à séparer les données (dans Order) et le comportement (dans l’extension function), ce qui s’aligne bien avec un style fonctionnel ou lorsque vous travaillez avec des DTO générées automatiquement (par exemple depuis OpenAPI ou Protobuf).
4. Extension Function sur Type Générique
Les extension function fonctionnent aussi bien avec les génériques, aidant à écrire des fonctions utilitaires pour List<T>, Collection<T>…
fun <T> List<T>.secondOrNull(): T? {
return if (this.size >= 2) this[1] else null
}
fun main() {
val customers = listOf("Alice", "Bob", "Charlie")
println(customers.secondOrNull())
}
List<T>.secondOrNull() est une generic extension function, fonctionnant avec tout type T. Le résultat :
Bob
Note: La standard library Kotlin possède déjà de nombreuses extension function utiles comme
firstOrNull(),associateBy(),groupBy(). Avant d’écrire les vôtres, vérifiez danskotlin.collectionssi elles existent déjà — évitez de « réinventer la roue ».
5. Utiliser les Extension Function à la Place des Classes Utilitaires
En Java, vous écrivez souvent des classes comme StringUtils, DateUtils avec des méthodes statiques. Kotlin encourage à les remplacer par des extension function car la syntaxe d’appel est plus naturelle.
Comparaison :
// Style Java
String formatted = DateUtils.formatToVietnamese(someDate);
// Style Kotlin avec extension function
fun LocalDate.toVietnameseFormat(): String {
val formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
return this.format(formatter)
}
val formatted = someDate.toVietnameseFormat()
L’approche Kotlin se lit de façon plus naturelle : « cette date, formatez-la à la manière vietnamienne » plutôt que « DateUtils, formatez cette date ». Lorsque vous l’utilisez dans un service Spring Boot, vous pouvez regrouper les extension function connexes dans un fichier séparé, par exemple DateExtensions.kt:
// DateExtensions.kt
package com.nonladev.util
import java.time.LocalDate
import java.time.format.DateTimeFormatter
fun LocalDate.toVietnameseFormat(): String =
this.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))
fun LocalDate.isWeekend(): Boolean {
val day = this.dayOfWeek
return day == java.time.DayOfWeek.SATURDAY || day == java.time.DayOfWeek.SUNDAY
}
Importez ces fonctions dans vos fichiers en utilisant import com.nonladev.util.toVietnameseFormat.
6. Extension Function avec Nullable Receiver
Un autre point fort : vous pouvez écrire une extension function pour un nullable type, aidant à gérer null de manière sûre sans avoir besoin de vérifications if répétées.
fun String?.orDefault(default: String): String {
return this ?: default
}
fun main() {
val customerName: String? = null
println(customerName.orDefault("Client de passage"))
}
Comme le receiver est String?, cette fonction peut être appelée même si la variable est null. À l’intérieur, l’opérateur Elvis ?: retourne default si this est null. Le résultat :
Client de passage
C’est un pattern très utile quand on travaille avec des données d’une API externe — où les champs peuvent être null mais vous voulez que la logique d’affichage ait toujours une valeur par défaut.
7. Tester les Extension Function
Les extension function sont des static methods cachés, vous les testez donc exactement comme toute fonction pure — sans avoir besoin de mock.
Ajoutez les dépendances JUnit 5 et AssertJ (si vous utilisez Maven pour votre module Kotlin) :
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>
Test pour Order.totalWithTax() :
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class OrderExtensionsTest {
@Test
fun `totalWithTax should add tax amount to subtotal`() {
val order = Order(id = 1, subtotal = 200.0, taxRate = 0.05)
val result = order.totalWithTax()
assertThat(result).isEqualTo(210.0)
}
}
Ce test n’a besoin de rien d’instancier de particulier — créez un Order, appelez la fonction, affirmez le résultat. C’est simple car l’extension function ne maintient pas d’état propre, elle opère seulement sur le receiver.
8. Erreurs Courantes et Quand Ne Pas les Utiliser
Les extension function ne peuvent pas surcharger les méthodes réelles. Si la classe d’origine a déjà une méthode du même nom et de la même signature, la méthode de la classe a toujours la priorité — le compilateur avertira « extension is shadowed ».
class Greeter {
fun greet() = "Hello from member function"
}
fun Greeter.greet() = "Hello from extension function"
fun main() {
println(Greeter().greet())
}
Le résultat est toujours :
Hello from member function
Les extension function sont résolues statiquement (static dispatch), pas selon le polymorphisme runtime. Si vous appelez une extension function via une variable typée comme classe parente mais l’instance réelle est une classe enfant, Kotlin utilise l’extension function de la classe parente — contrairement aux member functions qui utilisent le dynamic dispatch.
Note: N’abusez pas des extension function en ajoutant trop de fonctions aux classes de base comme
StringouAny. Le code peut devenir difficile à comprendre quant à l’origine des fonctions, surtout quand plusieurs fichiers étendent une même classe. Organisez clairement par package et nommez les fichiersXxxExtensions.kt.
Ne pas utiliser les extension function quand:
- Vous devez ajouter d’état (propriété avec backing field) — Kotlin ne permet pas aux extension properties d’avoir de vrais backing fields.
- La logique a besoin d’accéder aux membres privés de la classe d’origine — les extension functions ne voient que l’API publique/interne.
- La fonction a un sens métier fondamental pour l’objet domaine — dans ce cas, la mettre dans la classe elle-même pour assurer l’encapsulation.
9. Conclusion
Les extension function sont un outil puissant pour rendre le code Kotlin plus concis et plus naturel, particulièrement utile quand vous avez besoin d’ajouter du comportement à des classes existantes ou de séparer le comportement du modèle de données. Cependant, rappelez-vous : ce n’est que du sucre syntaxique au moment de la compilation, ce n’est pas un vrai remplacement pour l’héritage ou l’accès à l’état privé. Utilisez-les correctement — pour la logique utilitaire, le formatage, ou la gestion sûre de null — et vous aurez une base de code Kotlin beaucoup plus propre et lisible qu’avec l’approche traditionnelle Java des classes XxxUtils.