Contactez-nous

Objets singletons (`object`)

Apprenez à implémenter facilement le patron de conception singleton en Kotlin grâce à la déclaration `object`. Créez des instances uniques garanties.

Introduction : le besoin d'une instance unique

Dans la conception de logiciels, il existe un patron de conception (design pattern) très courant appelé singleton. L'objectif du singleton est de garantir qu'une classe ne possède qu'une seule et unique instance dans toute l'application, et de fournir un point d'accès global à cette instance unique.

Pourquoi voudrait-on limiter une classe à une seule instance ? Les cas d'usage typiques incluent :

  • Gestion de configuration : Charger et fournir les paramètres de configuration de l'application depuis un seul endroit.
  • Logging : Centraliser l'écriture des logs via une seule instance de logger.
  • Gestion de ressources partagées : Contrôler l'accès à une ressource unique comme un pool de connexions à une base de données ou un gestionnaire de cache.
  • Factories : Fournir un point centralisé pour créer d'autres objets.
  • Objets utilitaires : Regrouper des fonctions utilitaires statiques sans état.

Implémenter le singleton manuellement dans des langages comme Java peut être un peu verbeux et demande des précautions (gestion du multi-threading pour l'initialisation, constructeur privé, etc.). Kotlin, reconnaissant la fréquence de ce besoin, fournit une construction de langage directe et élégante pour créer des singletons : la déclaration d'objet avec le mot-clé `object`.

Déclaration d'objet : `object` à la place de `class`

Pour créer un singleton en Kotlin, vous utilisez simplement le mot-clé `object` là où vous utiliseriez normalement le mot-clé `class`. Cette déclaration fait deux choses en une : elle définit une structure (comme une classe, avec des propriétés et des méthodes) et elle crée immédiatement l'unique instance de cette structure.

La syntaxe est très simple :

object NomDuSingleton {
    // Propriétés
    val property1 = "Valeur constante"
    var mutableProperty = 0

    // Blocs d'initialisation
    init {
        println("Singleton $NomDuSingleton initialisé.")
        // Initialiser des ressources, charger la config, etc.
        mutableProperty = 10
    }

    // Méthodes
    fun doSomething() {
        println("Action du singleton. Propriété mutable: $mutableProperty")
    }

    fun getProperty1(): String {
        return property1
    }
}

Contrairement à une classe, vous n'instanciez jamais un `object` déclaré de cette manière en utilisant un constructeur (`NomDuSingleton()` est invalide). L'instance unique est créée automatiquement par le runtime Kotlin la première fois qu'elle est accédée (initialisation paresseuse et thread-safe).

Caractéristiques et fonctionnement

Les objets déclarés avec `object` ont plusieurs caractéristiques importantes :

  • Instance unique garantie : Le runtime Kotlin assure qu'il n'existera jamais plus d'une instance de cet objet.
  • Initialisation paresseuse (Lazy Initialization) : Le code dans le bloc `init` (et l'initialisation des propriétés) n'est généralement exécuté que la première fois que l'objet est référencé dans le code.
  • Initialisation Thread-Safe : L'initialisation de l'objet est garantie comme étant sûre même dans un environnement multi-threadé.
  • Pas de constructeurs : Les objets singletons ne peuvent pas avoir de constructeurs (ni primaire, ni secondaires) car leur création est gérée automatiquement par le système.
  • Peuvent hériter et implémenter : Tout comme les classes, les objets peuvent hériter d'une classe (une seule) et implémenter une ou plusieurs interfaces.
  • Accès direct : Vous accédez aux propriétés et méthodes de l'objet directement via son nom, sans avoir besoin de créer ou de récupérer une instance.

Accéder aux membres du singleton

Pour utiliser le singleton, vous faites référence à ses membres (propriétés et méthodes) en utilisant directement le nom de l'objet suivi de l'opérateur point (`.`). Cela ressemble à l'accès à des membres statiques en Java, mais en Kotlin, `NomDuSingleton` est une référence à la véritable instance unique de l'objet.

object AppConfiguration {
    val apiUrl = "https://api.example.com"
    var maxConnections = 10

    init {
        println("Chargement de la configuration...")
        // Simule le chargement depuis un fichier
    }

    fun printConfig() {
        println("API URL: $apiUrl")
        println("Max Connections: $maxConnections")
    }
}

fun main() {
    // Premier accès -> Initialisation (le message init s'affiche ici)
    println("Accès à la config pour la première fois...")
    val url = AppConfiguration.apiUrl 
    println("URL récupérée: $url")

    println("\nModification de la config...")
    AppConfiguration.maxConnections = 20 // Modifie la propriété de l'unique instance

    println("\nAffichage de la config...")
    // Second accès -> Pas de nouvelle initialisation
    AppConfiguration.printConfig()
    // Affiche :
    // API URL: https://api.example.com
    // Max Connections: 20

    // Récupération d'une autre propriété
    val maxConns = AppConfiguration.maxConnections
    println("\nConnexions max récupérées : $maxConns") // Affiche 20
}

Cas d'usage courants

La déclaration `object` est parfaite pour :

  • Configurations : Centraliser les paramètres constants ou chargeables.
  • Logging : Fournir un point d'accès unique pour enregistrer les messages.
  • object Logger {
        fun info(message: String) { println("[INFO] $message") }
        fun error(message: String) { println("[ERROR] $message") }
    }
    // Utilisation: Logger.info("App démarrée")
  • Constants et Utilitaires : Regrouper des constantes ou des fonctions utilitaires pures (sans état).
  • object MathConstants {
        const val PI = 3.14159
    }
    object StringUtils {
        fun isEmpty(s: String?) = s == null || s.isEmpty()
    }
    // Utilisation: val radius = 5 * MathConstants.PI; if (StringUtils.isEmpty(name)) {...}
  • Factories Simples :
  • interface Service
    class DefaultService : Service
    object ServiceFactory {
        fun createService(): Service {
            // Logique de création (peut être plus complexe)
            return DefaultService()
        }
    }
    // Utilisation: val myService = ServiceFactory.createService()

Distinction avec `object` expression et `companion object`

Il est important de noter que le mot-clé `object` a d'autres usages en Kotlin qui sont différents de la déclaration d'objet singleton :

  • Expressions d'objet (`object : Type { ... }`) : Utilisées pour créer des instances anonymes d'une interface ou d'une classe, souvent pour des listeners ou des callbacks. Ces expressions créent une nouvelle instance à chaque fois qu'elles sont évaluées (pas des singletons).
  • Objets compagnons (`companion object { ... }`) : Déclarés à l'intérieur d'une classe, ils permettent de définir des membres (propriétés, méthodes) qui sont liés à la classe elle-même plutôt qu'à ses instances, similaires aux membres statiques en Java. Bien qu'il n'y ait qu'une seule instance de l'objet compagnon par classe, son but est différent de celui d'un singleton applicatif global.

Cette section se concentre uniquement sur la déclaration `object NomDuSingleton { ... }` pour implémenter le patron singleton.

Récapitulatif : `object` pour des instances uniques

La déclaration d'objet avec le mot-clé `object` est la manière idiomatique et simple de créer des singletons en Kotlin :

  • Garantit une seule instance de l'objet dans toute l'application.
  • Combine la déclaration de la structure et la création de l'instance unique.
  • L'initialisation est paresseuse et thread-safe.
  • Pas de constructeurs à appeler, l'instance est gérée par le runtime.
  • Accès aux membres via le nom de l'objet directement (`NomObjet.membre`).
  • Idéal pour les configurations, loggers, factories, utilitaires globaux.

C'est un outil puissant pour gérer l'état et les ressources partagées de manière contrôlée et centralisée.