Les Design Patterns en Kotlin : Le Singleton
En général, si vous disposez d'une classe, vous pouvez en créer autant d'instances que vous le souhaitez, mais tel ne pas le cas pour un Singleton

En général, si vous disposez d'une classe, vous pouvez en créer autant d'instances que vous le souhaitez, mais tel ne pas le cas pour un Singleton, l’idée derrière ce dernier est d’avoir une seule instance d’un objet dans notre système, vous vous demandez peut-être pourquoi est-ce important, dans cet article, nous allons essayer de comprendre le pourquoi.
En Kotlin, pour créer une liste, on peut utiliser la fonction listOf()
, supposons que nous devons créer deux listes, une contenant les noms des fruits, une autre contenant nos fruits préférés, notre code ressemblera à ça :
val fruits: List<String> = listOf("Mangue","Avocat","Banane")
val fruitsFavoris = listOf("Mangue")
Là notre code n’a aucun problème. Maintenant, imaginez que vous ne connaissez aucun fruits (je sais que c’est banal comme exemples, mais imaginez quand même), vous allez créer deux listes vides.
val aucunFruit: List<String> = listOf()
val aucunFavori: List<String> = listOf()
Notez que ces deux listes sont exactement les mêmes parce qu'elles sont vides. Et elles resteront vides parce qu'elles sont immuables.
Étant donné que ces deux instances d'une classe sont les mêmes, il n'est pas très logique de les garder en mémoire plusieurs fois. Ce serait formidable si toutes les références à une liste vide pointaient vers la même instance d'un objet. En fait, c'est ce qui se passe avec null
, si l'on y réfléchit bien. Tous les null
sont identiques.
C'est l'idée principale du modèle de conception Singleton.
Il y a quelques exigences pour le design Pattern Singleton :
- Nous devons avoir exactement une instance dans notre système.
- Cette instance doit être accessible depuis n'importe quelle partie de notre système.
En Java et dans d'autres langages, cette tâche est assez complexe. Tout d'abord, il faut interdire la création de nouvelles instances d'un objet en créant un constructeur pour la classe privée. Ensuite, il faut s'assurer que l'instanciation est de préférence paresseuse, sûre pour les threads et performante, avec les exigences suivantes :
- Paresseuse : Il se peut que nous ne voulions pas instancier un objet singleton au démarrage de notre programme, car cette opération peut s'avérer coûteuse. Nous aimerions l'instancier uniquement lorsqu'il est nécessaire pour la première fois.
- Sûr pour les threads : Si deux threads essaient d'instancier un objet singleton en même temps, ils doivent recevoir la même instance et non deux instances différentes.
- Performant : Si plusieurs threads tentent d'instancier un objet singleton en même temps, nous ne devons pas les bloquer pendant une longue période de temps, car cela interromprait leur exécution.
Kotlin facilite la création de singletons en introduisant un mot-clé appelé object
. En utilisant ce mot-clé, nous obtiendrons une implémentation d'un objet singleton, qui répondra à toutes nos exigences.
Nous déclarons les objets comme une classe normale, mais sans constructeur, parce qu'un objet singleton ne peut pas être instancié par nous :
object ListeVide
Désormais, nous pouvons accéder à ListeVide
depuis n'importe quel endroit de notre code, et il n'y aura qu'une seule instance de cette liste :
val aucunFruit = ListeVide
val aucunFavori = ListeVide
Supposons que nous avons une fonction qui nous permet d’afficher notre liste des fruits
fun afficher(fruits: List<String>) {
for(fruit in fruits) {
println(fruit)
}
}
Lorsque nous passons notre liste initiale des fruits, le code se compile très bien :
fun afficher(fruitsFavoris)
Mais si nous lui passons notre liste vide, le code ne se compilera pas :
fun afficher(aucunFavori)
La raison en est que notre fonction n'accepte que des arguments de type liste de chaînes de caractères, alors que rien n'indique à la fonction que ListeVide
est de ce type.
Heureusement, en Kotlin, les objets singleton peuvent implémenter des interfaces, et une interface générique List
est disponible :
object ListeVide : List<String>
Maintenant, notre compilateur va nous demander d'implémenter les fonctions requises.
Nous le ferons en ajoutant un corps à l'objet :
object ListeVide : List<String> {
override val size = 0
override fun contains(element: String) = false
/... /
}
Nous vous laissons le soin d'implémenter les autres fonctions si vous le souhaitez. Cependant, vous n'êtes pas obligé de le faire car ce que nous venons de faire est l’implementation de la fonction emptyList
fournit déjà par Kotlin pour créer des listes vides de n'importe quel type :
internal object EmptyList : List<Nothing>, Serializable, RandomAccess {
private const val serialVersionUID: Long = -7390468764508069838L
override fun equals(other: Any?): Boolean = other is List<*> && other.isEmpty()
override fun hashCode(): Int = 1
override fun toString(): String = "[]"
override val size: Int get() = 0
override fun isEmpty(): Boolean = true
override fun contains(element: Nothing): Boolean = false
override fun containsAll(elements: Collection<Nothing>): Boolean = elements.isEmpty()
override fun get(index: Int): Nothing = throw IndexOutOfBoundsException("Empty list doesn't contain element at index $index.")
override fun indexOf(element: Nothing): Int = -1
override fun lastIndexOf(element: Nothing): Int = -1
override fun iterator(): Iterator<Nothing> = EmptyIterator
override fun listIterator(): ListIterator<Nothing> = EmptyIterator
override fun listIterator(index: Int): ListIterator<Nothing> {
if (index != 0) throw IndexOutOfBoundsException("Index: $index")
return EmptyIterator
}
override fun subList(fromIndex: Int, toIndex: Int): List<Nothing> {
if (fromIndex == 0 && toIndex == 0) return this
throw IndexOutOfBoundsException("fromIndex: $fromIndex, toIndex: $toIndex")
}
private fun readResolve(): Any = EmptyList
}
public fun <T> emptyList(): List<T> = EmptyList
vous pouvez voir le code sur le GitHub de Jetbrains à l’adresse suivante https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/src/kotlin/collections/Collections.kt
Ceci représente l’un des cas d’utilisation concerts du Singleton.
Une autre utilisation courante de ce Design Pattern et la connexion à la base de données, si vous avez une classe qui sert à établir la connexion entre votre application et la base de données, il n’est pas bon d’avoir plusieurs instances de cette classe, car lors qu’il faudra par exemple fermer la connexion à cette base de données, il faudra chercher par tout ou cette classe a été instance pour appeler la méthode qui va fermer la connexion, sinon ceci peut causer plusieurs problèmes, notamment des fuites mémoires.
Avant de finir
Un object
Kotlin présente une différence majeure par rapport à une classe : il ne peut pas avoir de constructeurs. Si vous devez mettre en œuvre l'initialisation de votre Singleton, comme le chargement des données d'un fichier de configuration pour la première fois, vous pouvez utiliser le bloc init
à la place :
object MonObjet {
init {
println("Hello World")
}
}
Notez que si un Singleton n'est jamais invoqué, il n'exécutera pas du tout sa logique d'initialisation, ce qui permet d'économiser des ressources.
C'est ce qu'on appelle l'initialisation paresseuse.