Génériques, objets et extensions

1. Introduction

Au fil des décennies, les programmeurs ont développé plusieurs fonctionnalités du langage de programmation pour vous aider à mieux coder, en utilisant moins de code pour exprimer une même idée, en ayant recours à des concepts abstraits pour exprimer des idées complexes ou encore en écrivant du code qui empêche les autres développeurs de faire des erreurs. Le langage Kotlin ne fait pas exception et plusieurs fonctionnalités permettent aux développeurs d'écrire un code plus expressif.

Malheureusement, ces dernières ne sont pas forcément accessibles si vous débutez en programmation. Même si elles semblent utiles, l'étendue de leur utilité et les problèmes qu'elles permettent de résoudre ne sont pas toujours évidents. Vous avez probablement déjà vu certaines fonctionnalités utilisées dans Compose et dans d'autres bibliothèques.

Bien que rien ne puisse remplacer l'expérience, cet atelier de programmation vous présente plusieurs concepts Kotlin pour vous aider à structurer des applications plus volumineuses, notamment :

  • les génériques ;
  • les différents types de classes (classes d'énumération et classes de données) ;
  • les objets singleton et compagnons ;
  • les propriétés et fonctions des extensions ;
  • les fonctions de portée.

À la fin de cet atelier de programmation, vous devriez mieux comprendre le code étudié durant ce cours, et avoir appris quelques situations au cours desquelles vous pourriez rencontrer ou utiliser ces concepts dans vos propres applications.

Conditions préalables

  • Avoir une bonne connaissance des concepts de programmation orientée objet, y compris l'héritage.
  • Savoir définir et implémenter des interfaces.

Points abordés

  • Comment définir un paramètre de type générique pour une classe ?
  • Comment instancier une classe générique ?
  • Quand utiliser des classes de données et d'énumération ?
  • Comment définir un paramètre de type générique devant implémenter une interface ?
  • Comment utiliser les fonctions de portée pour accéder aux propriétés et aux méthodes de classes ?
  • Comment définir des objets singleton et compagnons pour une classe ?
  • Comment étendre des classes existantes avec de nouvelles propriétés et méthodes ?

Ce dont vous avez besoin

  • Un navigateur Web ayant accès à Kotlin Playground

2. Créer une classe réutilisable avec des génériques

Imaginons que vous codiez une application pour un quiz en ligne, semblable aux quiz de ce cours. Les questions peuvent prendre plusieurs formes, telles que des textes à trous ou encore des "vrai ou faux". Une question individuelle peut être représentée par une classe, avec plusieurs propriétés.

Le texte d'une question peut être représenté par une chaîne. Les questions du quiz doivent également représenter la réponse. Toutefois, différents types de questions (vrai ou faux, par exemple) peuvent avoir besoin de représenter la réponse à l'aide d'un autre type de données. Définissons trois types de questions différents.

  • Question à trous : la réponse est un mot représenté par une donnée de type String.
  • Question de type Vrai ou faux : la réponse est représentée par une donnée de type Boolean.
  • Problèmes mathématiques​: la réponse est une valeur numérique. La réponse à un problème arithmétique simple est représentée par une donnée de type Int.

De plus, les questions de notre exemple, quel que soit leur type, auront un indice de difficulté. L'indice de difficulté est représenté par une chaîne avec trois valeurs possibles : "easy" (facile), "medium" (intermédiaire) ou "hard" (difficile).

Définissez des classes pour chaque type de question du quiz :

  1. Accédez à Kotlin Playground.
  2. Au-dessus de la fonction main(), définissez une classe nommée FillInTheBlankQuestion pour les questions à trous, composée d'une propriété String pour questionText (texte de la question), d'une propriété String pour answer (réponse) et d'une propriété String pour difficulty (difficulté).
class FillInTheBlankQuestion(
    val questionText: String,
    val answer: String,
    val difficulty: String
)
  1. Sous la classe FillInTheBlankQuestion, définissez une autre classe nommée TrueOrFalseQuestion pour les questions de type Vrai ou faux, composée d'une propriété String pour questionText, d'une propriété Boolean pour answer et d'une propriété String pour difficulty.
class TrueOrFalseQuestion(
    val questionText: String,
    val answer: Boolean,
    val difficulty: String
)
  1. Enfin, sous les deux autres classes, définissez une classe NumericQuestion, composée d'une propriété String pour questionText, d'une propriété Int pour answer et d'une propriété String pour difficulty.
class NumericQuestion(
    val questionText: String,
    val answer: Int,
    val difficulty: String
)
  1. Regardez le code que vous avez écrit. Avez-vous remarqué la répétition ?
class FillInTheBlankQuestion(
    val questionText: String,
    val answer: String,
    val difficulty: String
)

class TrueOrFalseQuestion(
    val questionText: String,
    val answer: Boolean,
    val difficulty: String
)
class NumericQuestion(
    val questionText: String,
    val answer: Int,
    val difficulty: String
)

Les trois classes ont exactement les mêmes propriétés : questionText, answer et difficulty. La seule différence réside dans le type de données de la propriété answer. Vous pensez peut-être que la solution évidente est de créer une classe parente avec questionText et difficulty, et que chaque sous-classe définisse la propriété de answer.

Toutefois, l'utilisation de l'héritage pose le même problème que ci-dessus. Chaque fois que vous ajoutez un nouveau type de question, vous devez ajouter une propriété de answer. La seule différence réside dans le type de données. Il est également étrange d'avoir une classe parente Question sans propriété de réponse.

Lorsque vous voulez qu'une propriété ait différents types de données, le sous-classement n'est pas la solution. Kotlin propose plutôt des types génériques qui vous permettent d'avoir une seule propriété avec différents types de données, selon les cas d'utilisation.

Qu'est-ce qu'un type de données générique ?

Les types génériques (ou génériques) permettent à un type de données, tel qu'une classe, de spécifier un type de données d'espace réservé inconnu pouvant être utilisé avec ses propriétés et méthodes. Qu'est-ce que cela signifie exactement ?

Dans l'exemple ci-dessus, au lieu de définir une propriété de réponse pour chaque type de données possible, vous pouvez créer une classe unique pour représenter n'importe quelle question et utiliser un nom d'espace réservé pour le type de données de la propriété answer. Le type de données réel (String, Int, Boolean, etc.) est spécifié lorsque la classe est instanciée. Chaque fois que le nom de l'espace réservé est utilisé, le type de données transmis à la classe est utilisé à la place. La syntaxe permettant de définir un type générique pour une classe est présentée ci-dessous :

67367d9308c171da.png

Un type de données générique est fourni lors de l'instanciation d'une classe. Il doit donc être défini dans la signature de la classe. Le nom de la classe est suivi d'un chevron ouvrant (<), puis d'un nom d'espace réservé pour le type de données, et enfin d'un chevron fermant (>).

Vous pouvez ensuite utiliser le nom de l'espace réservé chaque fois que vous utilisez un type de données réel dans la classe, pour une propriété par exemple.

81170899b2ca0dc9.png

Le processus est le même que pour toute autre déclaration de propriété, à la différence que le nom de l'espace réservé est utilisé à la place du type de données.

Comment votre classe peut-elle savoir quel type de données utiliser ? Le type de données utilisé pour le type générique est transmis comme un paramètre entre chevrons lorsque vous instanciez la classe.

9b8fce54cac8d1ea.png

Le nom de la classe est suivi d'un chevron ouvrant (<), puis du type de données réel (String, Boolean, Int, etc.), et enfin d'un chevron fermant. (>). Le type de données de la valeur que vous transmettez pour la propriété générique doit correspondre à celui entre les chevrons. La propriété "answer" sera générique, ce qui vous permettra d'utiliser une classe pour représenter tout type de question, que la réponse soit String, Boolean ou Int, ou tout autre type de données.

Refactoriser votre code pour utiliser des génériques

Refactorisez votre code pour utiliser une classe unique nommée Question avec une propriété de réponse générique.

  1. Supprimez les définitions de classe pour FillInTheBlankQuestion, TrueOrFalseQuestion et NumericQuestion.
  2. Créez une classe nommée Question.
class Question()
  1. Après le nom de la classe, mais avant les parenthèses, ajoutez un paramètre de type générique à l'aide des chevrons gauche et droit. Appelez le type générique T.
class Question<T>()
  1. Ajoutez les propriétés questionText, answer et difficulty. La propriété questionText doit être de type String. La propriété answer doit être de type T, car son type de données est spécifié lors de l'instanciation de la classe Question. La propriété difficulty doit être de type String.
class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: String
)
  1. Pour voir comment cela fonctionne avec plusieurs types de questions (questions à trous, vrai ou faux, etc.), créez trois instances de la classe Question dans main(), comme ci-dessous.
fun main() {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", "medium")
    val question2 = Question<Boolean>("The sky is green. True or false", false, "easy")
    val question3 = Question<Int>("How many days are there between full moons?", 28, "hard")
}
  1. Exécutez votre code pour vous assurer que tout fonctionne correctement. Vous devriez maintenant avoir trois instances de la classe Question, chacune avec des types de données différents pour la réponse, au lieu de trois classes différentes ou de recourir à l'héritage. Si vous souhaitez gérer des questions avec un autre type de réponse, vous pouvez réutiliser la même classe Question.

3. Utiliser une classe d'énumération

Dans la section précédente, vous avez défini une propriété de difficulté, avec trois valeurs possibles : "easy", "medium" et "hard". Cela fonctionne, mais quelques problèmes persistent.

  1. Si vous avez mal orthographié l'une des trois chaînes possibles, vous risquez d'introduire des bugs.
  2. Si les valeurs changent, par exemple "medium", est renommé "average", vous devrez alors mettre à jour toutes les utilisations de la chaîne.
  3. Rien ne vous empêche, vous ou un autre développeur, d'utiliser accidentellement une chaîne différente qui n'est pas l'une des trois valeurs valides.
  4. Le code est plus difficile à gérer si vous ajoutez des niveaux de difficulté supplémentaires.

Kotlin vous aide à résoudre ces problèmes avec un type de classe spécial : la classe d'énumération. Une classe d'énumération permet de créer des types avec un ensemble limité de valeurs possibles. Par exemple, les quatre directions cardinales (Nord, Sud, Est et Ouest) peuvent être représentées par une classe d'énumération. Utiliser d'autres directions n'est pas nécessaire (et le code ne devrait pas le permettre). La syntaxe d'une classe d'énumération est présentée ci-dessous.

f4bddb215eb52392.png

Chaque valeur possible d'une énumération est appelée constante d'énumération. Les constantes d'énumération sont placées entre accolades et séparées par une virgule. Par convention, les constantes s'écrivent en majuscules.

Pour faire référence à des constantes d'énumération, vous devez utiliser l'opérateur point.

f3cfa84c3f34392b.png

Utiliser une constante d'énumération

Modifiez votre code de sorte à utiliser une constante d'énumération, plutôt qu'une String, pour représenter la difficulté.

  1. Sous la classe Question, définissez une classe enum nommée Difficulty.
enum class Difficulty {
    EASY, MEDIUM, HARD
}
  1. Dans la classe Question, modifiez le type de données de la propriété difficulty en remplaçant String par Difficulty.
class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: Difficulty
)
  1. Lors de l'initialisation des trois questions, transmettez la constante d'énumération pour la difficulté.
val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

4. Utiliser une classe de données

Bon nombre des classes avec lesquelles vous avez travaillé jusqu'à présent, telles que les sous-classes de Activity, ont plusieurs méthodes pour effectuer différentes actions. Ces classes ne se limitent pas à représenter des données. Elles contiennent de nombreuses fonctionnalités.

Les classes telles que la classe Question ne contiennent quant à elles que des données. Elles ne possèdent aucune méthode permettant d'effectuer une action et peuvent être définies comme étant une classe de données. Définir une classe comme une classe de données permet au compilateur Kotlin de formuler certaines hypothèses et d'implémenter automatiquement certaines méthodes. Par exemple, toString() est appelé en arrière-plan par la fonction println(). Lorsque vous utilisez une classe de données, toString() et d'autres méthodes sont implémentées automatiquement en fonction des propriétés de la classe.

Pour définir une classe de données, il vous suffit d'ajouter le mot clé data devant class.

e7cd946b4ad216f4.png

Convertir Question en classe de données

Tout d'abord, vous verrez ce qui se passe lorsque vous essayez d'appeler une méthode telle que toString() sur une classe qui n'est pas une classe de données. Puis, vous convertirez Question en une classe de données, de sorte que chaque méthode soit implémentée par défaut.

  1. Dans main(), affichez le résultat de l'appel de la méthode toString() sur question1.
fun main() {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)
    println(question1.toString())
}
  1. Exécutez votre code. La sortie affiche le nom de la classe et un identifiant unique pour l'objet.
Question@37f8bb67
  1. Transformez Question en une classe de données en utilisant le mot clé data.
data class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: Difficulty
)
  1. Exécutez à nouveau votre code. En en faisant une classe de données, Kotlin peut déterminer comment afficher les propriétés de la classe lors de l'appel à toString().
Question(questionText=Quoth the raven ___, answer=nevermore, difficulty=MEDIUM)

Lorsqu'une classe est définie comme une classe de données, les méthodes suivantes sont implémentées :

  • equals()
  • hashCode() : vous rencontrerez cette méthode lorsque vous utiliserez certains types de collections.
  • toString()
  • componentN() : component1(), component2(), etc.
  • copy()

5. Utiliser un objet singleton

Il existe de nombreux scénarios dans lesquels vous voulez qu'une classe n'ait qu'une seule instance. Par exemple pour :

  1. afficher les statistiques du joueur dans un jeu mobile ;
  2. interagir avec un seul périphérique (par exemple, envoyer du contenu audio via une enceinte) ;
  3. connecter un objet à une source de données distante (telle qu'une base de données Firebase) ;
  4. s'authentifier, lorsqu'un seul utilisateur ne peut se connecter à la fois.

Dans les scénarios ci-dessus, vous auriez probablement dû utiliser une classe. Cependant, vous n'aurez à instancier qu'une seule instance de cette classe. S'il n'y a qu'un seul appareil physique ou si un seul utilisateur s'est connecté à la fois, il n'y aura aucune raison de créer plusieurs instances. Le fait d'avoir deux objets qui accèdent simultanément au même périphérique peut entraîner un comportement très étrange et des bugs.

Vous pouvez indiquer clairement dans votre code qu'un objet ne doit avoir qu'une seule instance en le définissant comme un singleton. Un singleton est une classe qui ne peut avoir qu'une seule instance. Kotlin propose une construction spéciale, appelée object, qui peut être utilisée pour créer une classe singleton.

Définir un objet singleton

645e8e8bbffbb5f9.png

La syntaxe d'un objet est semblable à celle d'une classe. Utilisez simplement le mot clé object au lieu de class. Un objet singleton ne peut pas avoir de constructeur, car vous ne pouvez pas créer d'instances directement. Toutes les propriétés sont définies entre accolades et reçoivent une valeur initiale.

Certains exemples donnés plus tôt peuvent ne pas sembler évidents, surtout si vous n'avez pas encore travaillé avec des périphériques spécifiques ou si vous n'avez pas encore été confronté à l'authentification dans vos applications. Toutefois, vous verrez des objets singleton à mesure que vous poursuivez votre développement Android. Voyons un exemple concret qui utilise un objet pour l'état de l'utilisateur et dans lequel une seule instance est nécessaire.

Pour un quiz, il serait intéressant de pouvoir suivre la progression de l'étudiant en ayant accès au nombre de questions auxquelles il a répondu. Vous n'aurez besoin que d'une instance de cette classe. Par conséquent, au lieu de la déclarer en tant que classe, déclarez-la en tant qu'objet singleton.

  1. Créez un objet nommé StudentProgress.
object StudentProgress {
}
  1. Dans cet exemple, nous supposons qu'il y a 10 questions au total et que trois d'entre elles ont reçu une réponse jusqu'à présent. Ajoutez deux propriétés Int : total avec une valeur de 10 et answered avec une valeur de 3.
object StudentProgress {
    var total: Int = 10
    var answered: Int = 3
}

Accéder à un objet singleton

Vous vous souvenez que vous ne pouvez pas créer directement une instance d'un objet singleton ? Comment pouvez-vous alors accéder à ses propriétés ?

Étant donné qu'il n'existe qu'une seule instance de StudentProgress à la fois, vous accédez à ses propriétés en faisant référence au nom de l'objet lui-même, suivi de l'opérateur point (.), puis du nom de la propriété.

1b610fd87e99fe25.png

Mettez à jour votre fonction main() pour accéder aux propriétés de l'objet singleton.

  1. Dans main(), ajoutez un appel à println() qui génère les questions answered et total à partir de l'objet StudentProgress.
fun main() {
    ...
    println("${StudentProgress.answered} of ${StudentProgress.total} answered.")
}
  1. Exécutez votre code pour vérifier que tout fonctionne correctement.
...
3 of 10 answered.

Déclarer des objets en tant qu'objets compagnons

Les classes et les objets dans Kotlin peuvent être définis à l'intérieur d'autres types et constituent un excellent moyen d'organiser votre code. Vous pouvez définir un objet singleton dans une autre classe à l'aide d'un objet compagnon. Un objet compagnon vous permet d'accéder, à partir de la classe, à ses propriétés et méthodes, pour une syntaxe plus concise. Cet accès est possible seulement si les propriétés et méthodes de l'objet appartiennent à cette classe.

Pour déclarer un objet compagnon, il vous suffit d'ajouter le mot clé companion devant object.

68b263904ec55f29.png

Vous allez créer une nouvelle classe appelée Quiz pour stocker les questions du quiz et faire de StudentProgress un objet compagnon de la classe Quiz.

  1. Sous l'énumération Difficulty, définissez une nouvelle classe nommée Quiz.
class Quiz {
}
  1. Déplacez question1, question2 et question3 depuis main() vers la classe Quiz. Vous devez également supprimer println(question1.toString()), si ce n'est pas déjà fait.
class Quiz {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

}
  1. Déplacez l'objet StudentProgress dans la classe Quiz.
class Quiz {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

    object StudentProgress {
        var total: Int = 10
        var answered: Int = 3
    }
}
  1. Marquez l'objet StudentProgress avec le mot clé companion.
companion object StudentProgress {
    var total: Int = 10
    var answered: Int = 3
}
  1. Mettez à jour l'appel à println() pour référencer les propriétés avec Quiz.answered et Quiz.total. Même si ces propriétés sont déclarées dans l'objet StudentProgress, elles sont accessibles grâce à la notation par points en n'utilisant que le nom de la classe Quiz.
fun main() {
    println("${Quiz.answered} of ${Quiz.total} answered.")
}
  1. Exécutez votre code pour en vérifier la sortie.
3 of 10 answered.

6. Étendre les classes avec de nouvelles propriétés et méthodes

Lorsque vous utilisez Compose, vous avez peut-être remarqué une syntaxe intéressante lorsque vous spécifiez la taille des éléments d'interface utilisateur. Les types numériques, comme Double, semblent avoir des propriétés telles que dp et sp qui spécifient les dimensions.

a25c5a0d7bb92b60.png

Pourquoi les concepteurs du langage Kotlin incluent-ils des propriétés et des fonctions sur les types de données intégrés, en particulier pour la création d'une UI Android ? Ont-ils prédit l'avenir ? Kotlin était-il conçu pour être utilisé avec Compose avant même que ce dernier n'existe ?

Bien sûr que non ! Lorsque vous saisissez une classe, vous ne savez généralement pas exactement comment un autre développeur l'utilisera ou prévoit de l'utiliser dans son application. Il est impossible de prédire tous les cas d'utilisation futurs et c'est pourquoi il est déconseillé d'ajouter des données inutiles à votre code.

Le langage Kotlin permet aux autres développeurs d'étendre les types de données existants, en ajoutant des propriétés et des méthodes accessibles à l'aide d'une syntaxe à points, comme s'ils faisaient partie de ce type de données. Un développeur qui n'a pas travaillé sur les types à virgule flottante dans Kotlin, par exemple quelqu'un qui crée la bibliothèque Compose, peut décider d'ajouter des propriétés et des méthodes propres aux dimensions de l'interface utilisateur.

Comme vous avez vu cette syntaxe au cours des deux premières unités dédiées à l'apprentissage de Compose, il est maintenant temps de découvrir son fonctionnement. Vous allez ajouter des propriétés et des méthodes pour étendre les types existants.

Ajouter une propriété d'extension

Pour définir une propriété d'extension, ajoutez le nom du type et un opérateur point (.) devant le nom de la variable.

1e8a52e327fe3f45.png

Vous allez refactoriser le code de la fonction main() pour afficher la progression du quiz avec une propriété d'extension.

  1. Sous la classe Quiz, définissez une propriété d'extension de Quiz.StudentProgress nommée progressText de type String.
val Quiz.StudentProgress.progressText: String
  1. Définissez un getter pour la propriété d'extension qui renvoie la même chaîne que celle utilisée précédemment dans main().
val Quiz.StudentProgress.progressText: String
    get() = "${answered} of ${total} answered"
  1. Remplacez le code de la fonction main() par le code qui affiche progressText. Comme il s'agit d'une propriété d'extension de l'objet compagnon, vous pouvez y accéder grâce à la notation par points, en utilisant le nom de la classe, Quiz.
fun main() {
    println(Quiz.progressText)
}
  1. Exécutez votre code pour vérifier qu'il fonctionne.
3 of 10 answered.

Ajouter une fonction d'extension

Pour définir une fonction d'extension, ajoutez le nom du type et un opérateur point (.) avant le nom de la fonction.

879ff2761e04edd9.png

Vous allez ajouter une fonction d'extension pour afficher la progression du quiz sous forme d'une barre de progression. Étant donné que vous ne pouvez pas créer de barre de progression dans Kotlin Playground, vous allez afficher une barre de progression de style rétro grâce à du texte !

  1. Ajoutez une fonction d'extension à l'objet StudentProgress nommé printProgressBar(). La fonction ne doit pas comporter de paramètres ni de valeur de retour.
fun Quiz.StudentProgress.printProgressBar() {
}
  1. Affichez le caractère , le nombre correspondant à answered, en utilisant repeat(). Cette partie foncée de la barre de progression représente le nombre de questions auxquelles vous avez répondu. Utilisez print(), car vous ne voulez pas saisir de nouvelle ligne après chaque caractère.
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
}
  1. Affichez le caractère , le nombre correspondant à la différence entre total et answered, en utilisant repeat(). Cette partie claire représente les questions restantes dans la barre de processus.
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
}
  1. Affichez une nouvelle ligne à l'aide de println() sans argument, puis affichez progressText.
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(Quiz.progressText)
}
  1. Mettez à jour le code dans main() pour appeler printProgressBar().
fun main() {
    Quiz.printProgressBar()
}
  1. Exécutez votre code pour en vérifier la sortie.
▓▓▓▒▒▒▒▒▒▒
3 of 10 answered.

Tout ceci est-il obligatoire ? Absolument pas. Toutefois, les propriétés et méthodes d'extension vous permettent de partager davantage votre code avec d'autres développeurs. Avoir recours à une syntaxe utilisant des points pour d'autres types peut faciliter la lecture de votre code, à la fois pour vous, mais aussi pour les autres développeurs.

7. Réécrire les fonctions d'extension à l'aide d'interfaces

Sur la page précédente, vous avez vu comment ajouter des propriétés et des méthodes à l'objet StudentProgress sans avoir à inclure directement du code, à l'aide des propriétés et des fonctions d'extension. Bien qu'il s'agisse d'un excellent moyen d'ajouter des fonctionnalités à une classe déjà définie, il n'est pas toujours nécessaire d'étendre une classe si vous avez accès au code source. De même, vous ne savez pas toujours quelle implémentation adopter. Vous savez seulement qu'une certaine méthode ou propriété doit exister. Si plusieurs classes doivent avoir les mêmes propriétés et méthodes supplémentaires, éventuellement avec un comportement différent, vous pouvez définir ces propriétés et méthodes à l'aide d'une interface.

Supposons qu'en plus des quiz, vous disposiez de classes pour des sondages, les étapes d'une recette ou toute autre donnée ordonnée pouvant utiliser une barre de progression. Vous pouvez définir ce que l'on appelle une "interface" afin de spécifier les méthodes et/ou les propriétés que chacune de ces classes doit inclure.

eeed58ed687897be.png

Une interface est définie à l'aide du mot clé interface, suivi d'un nom UpperCamelCase (majuscule à la première lettre), puis d'une accolade ouvrante et d'une accolade fermante. Entre les accolades, vous pouvez définir n'importe quelle signature de méthode ou propriété "get-only" que toute classe conforme à l'interface doit implémenter.

6b04a8f50b11f2eb.png

Une interface est un contrat. On considère qu'une classe qui se conforme à une interface étend l'interface. Une classe peut déclarer qu'elle souhaite étendre une interface à l'aide du signe deux-points (:), suivi d'une espace, puis du nom de l'interface.

78af59840c74fa08.png

De son côté, la classe doit implémenter toutes les propriétés et méthodes spécifiées dans l'interface. Cela vous permet de vous assurer facilement que toute classe devant étendre l'interface implémente exactement les mêmes méthodes avec la même signature de méthode. Si vous modifiez l'interface de quelque manière que ce soit, par exemple en ajoutant ou en supprimant des propriétés ou des méthodes, ou en changeant la signature d'une méthode, le compilateur exige que vous mettiez à jour toute classe qui étend l'interface. De la sorte, votre code reste cohérent et plus facile à gérer.

Les interfaces permettent de modifier le comportement des classes qui les étendent. Il revient à chaque classe de fournir l'implémentation.

Voyons comment réécrire la barre de progression pour qu'elle utilise une interface et comment faire en sorte que la classe Quiz étende cette interface.

  1. Au-dessus de la classe Quiz, définissez une interface nommée ProgressPrintable. Nous avons choisi le nom ProgressPrintable, car il permet aux classes qui l'étendent d'afficher une barre de progression.
interface ProgressPrintable {
}
  1. Dans l'interface ProgressPrintable, définissez une propriété nommée progressText.
interface ProgressPrintable {
    val progressText: String
}
  1. Modifiez la déclaration de la classe Quiz pour qu'elle étende l'interface ProgressPrintable.
class Quiz : ProgressPrintable {
    ...
}
  1. Dans la classe Quiz, ajoutez une propriété nommée progressText de type String, comme spécifié dans l'interface ProgressPrintable. Étant donné que la propriété provient de ProgressPrintable, faites précéder val du mot clé de remplacement.
override val progressText: String
  1. Copiez le "getter" de propriété de l'ancienne propriété d'extension progressText.
override val progressText: String
        get() = "${answered} of ${total} answered"
  1. Supprimez l'ancienne propriété d'extension progressText.

Code à supprimer :

val Quiz.StudentProgress.progressText: String
    get() = "${answered} of ${total} answered"
  1. Dans l'interface ProgressPrintable, ajoutez une méthode nommée printProgressBar qui n'accepte aucun paramètre et qui n'a pas de valeur renvoyée.
interface ProgressPrintable {
    val progressText: String
    fun printProgressBar()
}
  1. Dans la classe Quiz, ajoutez la méthode printProgressBar() à l'aide du mot clé override.
override fun printProgressBar() {
}
  1. Déplacez le code de l'ancienne fonction d'extension printProgressBar() vers le nouvel élément printProgressBar() à partir de l'interface. Modifiez la dernière ligne pour faire référence à la nouvelle variable progressText dans l'interface en supprimant la référence à Quiz.
override fun printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(progressText)
}
  1. Supprimez la fonction d'extension printProgressBar(). Cette fonctionnalité appartient désormais à la classe Quiz qui étend ProgressPrintable.

Code à supprimer :

fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(Quiz.progressText)
}
  1. Mettez à jour le code dans main(). Comme la fonction printProgressBar() est désormais une méthode de la classe Quiz, vous devez d'abord instancier un objet Quiz, puis appeler printProgressBar().
fun main() {
    Quiz().printProgressBar()
}
  1. Exécutez votre code. La sortie reste inchangée, mais votre code est désormais plus modulaire. À mesure que vos codebases se développent, vous pouvez facilement ajouter des classes conformes à la même interface pour réutiliser le code sans hériter d'une super-classe.
▓▓▓▒▒▒▒▒▒▒
3 of 10 answered.

Il existe de nombreux cas d'utilisation d'interfaces permettant de structurer votre code. Vous allez commencer à les utiliser fréquemment dans les modules courants. Voici quelques exemples d'interfaces que vous pourrez rencontrer lorsque vous utiliserez Kotlin.

  • Injection manuelle de dépendances : créez une interface définissant toutes les propriétés et méthodes de la dépendance. Imposez l'interface comme type de données de la dépendance (activité, scénario de test, etc.) afin qu'une instance de toute classe mettant en œuvre cette interface puisse être utilisée. Cela vous permet de permuter les implémentations sous-jacentes.
  • Simulation des tests automatisés : la classe fictive et la classe réelle sont conformes à la même interface.
  • Accès aux mêmes dépendances dans une application multiplateforme Compose : par exemple, créez une interface fournissant un ensemble commun de propriétés et de méthodes pour Android et pour ordinateur, même si l'implémentation sous-jacente diffère pour chaque plate-forme.
  • Plusieurs types de données dans Compose, tels que Modifier, sont des interfaces. Cela vous permet d'ajouter des modificateurs sans avoir à accéder au code source sous-jacent ni à le modifier.

8. Utiliser des fonctions de portée pour accéder aux propriétés et aux méthodes de classe

Comme vous l'avez déjà vu, Kotlin inclut de nombreuses fonctionnalités permettant de rendre votre code plus concis.

Parmi elles, vous découvrirez les fonctions de portée à mesure que vous poursuivez votre apprentissage. Les fonctions de portée vous permettent d'accéder rapidement aux propriétés et aux méthodes d'une classe sans avoir à accéder plusieurs fois au nom de la variable. Qu'est-ce que cela signifie exactement ? Prenons un exemple.

Éliminer les références d'objets répétitives avec des fonctions de portée

Les fonctions de portée sont des fonctions de niveau supérieur qui vous permettent d'accéder aux propriétés et aux méthodes d'un objet sans faire référence au nom de l'objet. Ces fonctions sont appelées "fonctions de portée", car le corps de la fonction transmise reprend la portée de l'objet avec lequel la fonction est appelée. Par exemple, certaines fonctions de portée vous permettent d'accéder aux propriétés et aux méthodes d'une classe, comme si ces fonctions étaient définies comme une méthode de cette classe. Cela peut rendre votre code plus lisible en vous permettant d'omettre le nom de l'objet lorsqu'il est redondant.

Pour mieux illustrer cela, nous allons examiner quelques fonctions de portée que vous étudierez plus tard dans le cours.

Remplacer les noms d'objets longs à l'aide de let()

La fonction let() vous permet de faire référence à un objet dans une expression lambda à l'aide de l'identifiant it, au lieu du nom réel de l'objet. Cela peut vous permettre d'éviter d'utiliser un nom d'objet long et plus descriptif lorsque vous accédez à plusieurs propriétés. La fonction let() est une fonction d'extension qui peut être appelée sur n'importe quel objet Kotlin à l'aide de la notation par points.

Essayez d'accéder aux propriétés de question1, question2 et question3 avec let() :

  1. Ajoutez une fonction à la classe Quiz nommée printQuiz().
fun printQuiz() {

}
  1. Ajoutez le code suivant pour afficher les éléments questionText, answer et difficulty de la question. Même si vous accédez à plusieurs propriétés pour question1, question2 et question3, le nom complet de la variable est utilisé à chaque fois. Si le nom de la variable a changé, vous devez mettre à jour chaque occurrence.
fun printQuiz() {
    println(question1.questionText)
    println(question1.answer)
    println(question1.difficulty)
    println()
    println(question2.questionText)
    println(question2.answer)
    println(question2.difficulty)
    println()
    println(question3.questionText)
    println(question3.answer)
    println(question3.difficulty)
    println()
}
  1. Utilisez un appel à la fonction let() pour encadrer le code accédant aux propriétés questionText, answer et difficulty de question1, question2 et question3. Remplacez le nom de la variable dans chaque expression lambda par cet appel.
fun printQuiz() {
    question1.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
    question2.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
    question3.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
}
  1. Mettez à jour le code dans main() pour créer une instance de la classe Quiz nommée quiz.
fun main() {
    val quiz = Quiz()
}
  1. Appelez printQuiz().
fun main() {
    val quiz = Quiz()
    quiz.printQuiz()
}
  1. Exécutez votre code pour vérifier que tout fonctionne correctement.
Quoth the raven ___
nevermore
MEDIUM

The sky is green. True or false
false
EASY

How many days are there between full moons?
28
HARD

Appeler les méthodes d'un objet sans variable avec apply()

L'une des fonctionnalités intéressantes des fonctions de portée est que vous pouvez les appeler sur un objet avant même qu'il ne soit attribué à une variable. Par exemple, la fonction apply() est une fonction d'extension qui peut être appelée sur un objet à l'aide de la notation par points. La fonction apply() renvoie également une référence à cet objet pour qu'il puisse être stocké dans une variable.

Mettez à jour le code dans main() pour appeler la fonction apply().

  1. Lors de la création de l'instance de la classe Quiz, appelez apply() après la parenthèse fermante. Vous pouvez omettre les parenthèses lorsque vous appelez apply() et utiliser la syntaxe lambda de fin.
val quiz = Quiz().apply {
}
  1. Déplacez l'appel à printQuiz() dans l'expression lambda. Vous n'avez plus besoin de référencer la variable quiz ni d'utiliser la notation par points.
val quiz = Quiz().apply {
    printQuiz()
}
  1. La fonction apply() renvoie l'instance de la classe Quiz, mais comme vous ne l'utilisez plus, supprimez la variable quiz. Avec la fonction apply(), vous n'avez même pas besoin d'une variable pour appeler des méthodes sur l'instance de Quiz.
Quiz().apply {
    printQuiz()
}
  1. Exécutez votre code. Notez que vous avez pu appeler cette méthode sans aucune référence à l'instance de Quiz. La fonction apply() a renvoyé les objets qui étaient stockés dans quiz.
Quoth the raven ___
nevermore
MEDIUM

The sky is green. True or false
false
EASY

How many days are there between full moons?
28
HARD

Bien que l'utilisation des fonctions de portée ne soit pas obligatoire pour obtenir la sortie souhaitée, les exemples ci-dessus montrent comment elles peuvent rendre votre code plus concis et éviter toute répétition d'un même nom de variable.

Le code ci-dessus n'est qu'un exemple. Nous vous encourageons à ajouter des favoris et à consulter la documentation relative aux fonctions de portée, dont vous découvrirez l'utilisation plus tard dans le cours.

9. Résumé

Vous venez de voir plusieurs nouvelles fonctionnalités Kotlin en action. Les génériques permettent de transmettre des types de données en tant que paramètres aux classes. Les classes d'énumération définissent quant à elles un ensemble limité de valeurs possibles, tandis que les classes de données permettent de générer automatiquement des méthodes utiles pour les classes.

Vous avez également vu comment créer un objet singleton (limité à une instance), comment en faire un objet compagnon d'une autre classe et comment étendre des classes existantes avec de nouvelles propriétés "get-only" et de nouvelles méthodes. Enfin, vous avez vu comment les fonctions de portée peuvent simplifier la syntaxe pour accéder aux propriétés et aux méthodes.

Ces concepts seront abordés dans les prochaines unités, à mesure que vous en apprendrez plus sur Kotlin, le développement Android et Compose. À présent, vous comprenez mieux comment fonctionnent ces concepts et comment ils peuvent améliorer la réutilisation et la lisibilité de votre code.

10. En savoir plus