Patrons de conception en Kotlin avec exemples : niveau avancé
\n\nLes patrons de conception sont des solutions éprouvées aux problèmes courants du développement logiciel. Si vous maîtrisez déjà les patrons de base (Singleton, Factory, Observer), il est temps de passer au niveau supérieur. Dans cet article, nous allons analyser quatre patrons avancés — Chaîne de responsabilité, Interpréteur, Médiateur et Mémento — et montrer leur implémentation en Kotlin en tenant compte des idiomes du langage (sealed class, extension functions, lambdas).
\n\n1. Chaîne de responsabilité (Chain of Responsibility)
\n\nObjectif : Permet de transmettre une requête séquentiellement le long d'une chaîne de gestionnaires jusqu'à ce que l'un d'eux la traite.
\n\nProblème : Un système de journalisation où les messages de différents niveaux (DEBUG, INFO, ERROR) doivent être traités par différents enregistreurs.
\n\nImplémentation en Kotlin :
\n\n// Gestionnaire abstrait\nabstract class Logger(private val level: Int) {\n private var nextLogger: Logger? = null\n\n fun setNext(logger: Logger): Logger {\n this.nextLogger = logger\n return logger\n }\n\n fun log(level: Int, message: String) {\n if (this.level <= level) {\n write(message)\n }\n nextLogger?.log(level, message)\n }\n\n protected abstract fun write(message: String)\n}\n\n// Gestionnaires concrets\nclass ConsoleLogger(level: Int) : Logger(level) {\n override fun write(message: String) = println("[Console] $message")\n}\n\nclass FileLogger(level: Int) : Logger(level) {\n override fun write(message: String) = println("[File] $message")\n}\n\nclass EmailLogger(level: Int) : Logger(level) {\n override fun write(message: String) = println("[Email] $message")\n}\n\n// Utilisation\nfun main() {\n val logger = ConsoleLogger(1).apply {\n setNext(FileLogger(2))\n .setNext(EmailLogger(3))\n }\n logger.log(2, "Avertissement : niveau de mémoire faible")\n // Sortie :\n // [Console] Avertissement : niveau de mémoire faible\n // [File] Avertissement : niveau de mémoire faible\n}\n\nAvantages en Kotlin : Grâce à apply et à l'appel chaîné de setNext, le code est concis. On peut également remplacer la classe abstraite par une interface avec une implémentation par défaut.
2. Interpréteur (Interpreter)
\n\nObjectif : Définit la grammaire d'un langage et interprète les phrases de ce langage.
\n\nProblème : Besoin d'analyser et d'évaluer des expressions arithmétiques simples (par exemple, "3 + 5 - 2").
\n\nImplémentation en Kotlin avec sealed class :
\n\n// Expression abstraite\nsealed class Expression {\n abstract fun interpret(): Int\n}\n\n// Expression terminale — nombre\ndata class Number(val value: Int) : Expression() {\n override fun interpret() = value\n}\n\n// Expressions non terminales — opérations\ndata class Add(val left: Expression, val right: Expression) : Expression() {\n override fun interpret() = left.interpret() + right.interpret()\n}\n\ndata class Subtract(val left: Expression, val right: Expression) : Expression() {\n override fun interpret() = left.interpret() - right.interpret()\n}\n\n// Analyseur (simplifié)\nfun parse(tokens: List<String>): Expression {\n val stack = mutableListOf<Expression>()\n var i = 0\n while (i < tokens.size) {\n when (tokens[i]) {\n "+" -> {\n val right = Number(tokens[++i].toInt())\n val left = stack.removeLast()\n stack.add(Add(left, right))\n }\n "-" -> {\n val right = Number(tokens[++i].toInt())\n val left = stack.removeLast()\n stack.add(Subtract(left, right))\n }\n else -> stack.add(Number(tokens[i]