Kotlin Bootcamp for Programmers 4: Programmation orientée objet

Cet atelier de programmation fait partie du cours Kotlin Bootcamp for Programmers (Formation Kotlin pour les programmeurs). Vous tirerez le meilleur parti de ce cours si vous suivez les ateliers de programmation dans l'ordre. En fonction de vos connaissances, vous pouvez parcourir certaines sections. Ce cours s'adresse aux programmeurs qui connaissent un langage orienté objet et souhaitent apprendre l'Kotlin.

Présentation

Dans cet atelier de programmation, vous allez créer un programme Kotlin et en apprendre davantage sur les classes et les objets en langage Kotlin. Vous connaîtrez une grande partie de ce contenu si vous connaissez un autre langage orienté objets, mais Kotlin présente des différences importantes permettant de réduire la quantité de code à écrire. Vous découvrirez aussi les classes abstraites et la délégation d'interface.

Plutôt que de créer un exemple d'application, les leçons de ce cours sont conçues pour développer vos connaissances, mais elles ne doivent pas être indépendantes les unes des autres pour vous permettre de parcourir les sections que vous connaissez déjà. Pour les assembler, de nombreux exemples utilisent un thème d'aquarium. Et si vous voulez voir l'histoire complète de l'aquarium, consultez le cours Udacity Kotlin Bootcamp for Programmers.

Ce que vous devez déjà savoir

  • Principes de base de Kotlin, y compris les types, les opérateurs et la boucle
  • Syntaxe de la fonction Kotlin
  • Principes de base de la programmation orientée objets
  • Principes de base d'un IDE, comme IntelliJ IDEA ou Android Studio

Points abordés

  • Créer des classes et accéder à des propriétés dans Kotlin
  • Créer et utiliser des constructeurs de classes en langage Kotlin
  • Créer une sous-classe et comment l'héritage fonctionne
  • À propos des classes abstraites, des interfaces et de la délégation d'interface
  • Créer et utiliser des classes de données
  • Utiliser des singletons, des énumérations et des classes scellées

Objectifs de l'atelier

  • Créer une classe avec des propriétés
  • Créer un constructeur pour une classe
  • Créer une sous-classe
  • Examiner des exemples de classes abstraites et d'interfaces
  • Créer une classe de données simple
  • En savoir plus sur les singletons, les énumérations et les classes scellées

Vous devez déjà connaître les termes de programmation suivants:

  • Les classes sont des plans des objets. Par exemple, une classe Aquarium correspond au plan de création d'un objet d'aquarium.
  • Les objets sont des instances de classes. Un objet aquarium est un véritable Aquarium.
  • Les propriétés sont des caractéristiques de classes, telles que la longueur, la largeur et la hauteur d'une Aquarium.
  • Les méthodes, également appelées fonctions de membre, font partie des fonctionnalités de la classe. Les méthodes sont ce que vous pouvez faire avec l'objet. Par exemple, vous pouvez fillWithWater() un objet Aquarium.
  • Une interface est une spécification qu'une classe peut implémenter. Par exemple, le nettoyage est courant pour les objets autres que les aquariums. Le nettoyage est généralement similaire pour différents objets. Vous pouvez donc avoir une interface appelée Clean qui définit une méthode clean(). La classe Aquarium peut implémenter l'interface Clean pour nettoyer l'aquarium avec une éponge douce.
  • Les packages permettent de regrouper du code associé afin de l'organiser ou de créer une bibliothèque de code. Une fois qu'un package a été créé, vous pouvez en importer le contenu dans un autre fichier et réutiliser le code et les classes qui s'y trouvent.

Dans cette tâche, vous allez créer un package et une classe comportant des propriétés et une méthode.

Étape 1: Créez un package

Les packages vous aident à organiser votre code.

  1. Dans le volet Project (Projet), sous le projet Hello Kotlin, effectuez un clic droit sur le dossier src.
  2. Sélectionnez New > Package (Nouveau &gt ; package) et nommez-le example.myapp.

Étape 2: Créer une classe avec des propriétés

Les classes sont définies avec le mot clé class, et les noms de classe par convention commencent par une lettre majuscule.

  1. Effectuez un clic droit sur le package example.myapp.
  2. Sélectionnez New > Kotlin File / Class (Nouveau fichier/Classe Kotlin).
  3. Sous Kind (Genre), sélectionnez Class (Classe), puis nommez la classe Aquarium. IntelliJ IDEA inclut le nom du package dans le fichier et crée une classe Aquarium vide pour vous.
  4. Dans la classe Aquarium, définissez et initialisez les propriétés var pour la largeur, la hauteur et la longueur (en centimètres). Initialisez les propriétés avec les valeurs par défaut.
package example.myapp

class Aquarium {
    var width: Int = 20
    var height: Int = 40
    var length: Int = 100
}

En arrière-plan, Kotlin crée automatiquement des getters et des setters pour les propriétés que vous avez définies dans la classe Aquarium. Vous pouvez ainsi accéder directement aux propriétés, par exemple, myAquarium.length.

Étape 3: Créez une fonction "main()"

Créez un fichier appelé main.kt pour contenir la fonction main().

  1. Dans le volet Project (Projet) de gauche, faites un clic droit sur le package example.myapp.
  2. Sélectionnez New > Kotlin File / Class (Nouveau fichier/Classe Kotlin).
  3. Dans le menu déroulant Kind (Genre), conservez la sélection sous File (Fichier) et nommez le fichier main.kt. IntelliJ IDEA inclut le nom du package, mais n'inclut pas la définition de classe d'un fichier.
  4. Définissez une fonction buildAquarium() et, dans ce dernier, créez une instance de Aquarium. Pour créer une instance, référencez la classe comme s'il s'agissait d'une fonction Aquarium(). Cela appelle le constructeur de la classe et crée une instance de la classe Aquarium, comme pour l'utilisation de new dans d'autres langages.
  5. Définissez une fonction main() et appelez buildAquarium().
package example.myapp

fun buildAquarium() {
    val myAquarium = Aquarium()
}

fun main() {
    buildAquarium()
}

Étape 4: Ajoutez une méthode

  1. Dans la classe Aquarium, ajoutez une méthode pour imprimer les propriétés de la dimension "aquarium".
    fun printSize() {
        println("Width: $width cm " +
                "Length: $length cm " +
                "Height: $height cm ")
    }
  1. Dans main.kt, dans buildAquarium(), appelez la méthode printSize() sur myAquarium.
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
}
  1. Exécutez votre programme en cliquant sur le triangle vert à côté de la fonction main(). Observez le résultat.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
  1. Dans buildAquarium(), ajoutez du code pour définir la hauteur sur 60 et imprimez les propriétés de la dimension modifiée.
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
    myAquarium.height = 60
    myAquarium.printSize()
}
  1. Exécutez votre programme et observez le résultat.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 100 cm Height: 60 cm 

Dans cette tâche, vous allez créer un constructeur pour la classe et continuer à utiliser des propriétés.

Étape 1: Créez un constructeur

Au cours de cette étape, vous allez ajouter un constructeur à la classe Aquarium que vous avez créée à la première tâche. Dans l'exemple précédent, chaque instance de Aquarium est créée avec les mêmes dimensions. Vous pouvez modifier les dimensions une fois qu'elles sont créées en définissant les propriétés. Toutefois, il est plus simple de définir la taille appropriée au départ.

Dans certains langages de programmation, le constructeur est défini en créant une méthode portant le même nom que la classe. En langage Kotlin, vous définissez le constructeur directement dans la déclaration de la classe, en spécifiant les paramètres entre parenthèses comme si la classe était une méthode. Comme avec les fonctions en Kotlin, ces paramètres peuvent inclure des valeurs par défaut.

  1. Dans la classe Aquarium que vous avez créée précédemment, modifiez la définition de la classe pour inclure trois paramètres constructeur avec des valeurs par défaut pour length, width et height, puis attribuez-les aux propriétés correspondantes.
class Aquarium(length: Int = 100, width: Int = 20, height: Int = 40) {
   // Dimensions in cm
   var length: Int = length
   var width: Int = width
   var height: Int = height
...
}
  1. La solution Kotlin plus compacte consiste à définir les propriétés directement avec le constructeur, à l'aide de var ou de val. De plus, Kotlin crée automatiquement des getters et des setters. Vous pouvez ensuite supprimer les définitions de propriétés dans le corps de la classe.
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
  1. Lorsque vous créez un objet Aquarium avec ce constructeur, vous pouvez ne spécifier aucun argument et obtenir les valeurs par défaut, spécifier certains d'entre eux ou les spécifier tous, puis créer un objet Aquarium de taille personnalisée. Dans la fonction buildAquarium(), essayez différentes manières de créer un objet Aquarium à l'aide de paramètres nommés.
fun buildAquarium() {
    val aquarium1 = Aquarium()
    aquarium1.printSize()
    // default height and length
    val aquarium2 = Aquarium(width = 25)
    aquarium2.printSize()
    // default width
    val aquarium3 = Aquarium(height = 35, length = 110)
    aquarium3.printSize()
    // everything custom
    val aquarium4 = Aquarium(width = 25, height = 35, length = 110)
    aquarium4.printSize()
}
  1. Exécutez le programme et observez le résultat.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 25 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 110 cm Height: 35 cm 
Width: 25 cm Length: 110 cm Height: 35 cm 

Notez que vous n'avez pas dû surcharger le constructeur et écrire une version différente pour chacun de ces cas (plus quelques autres pour les autres combinaisons). Kotlin crée les éléments nécessaires à partir des valeurs par défaut et des paramètres nommés.

Étape 2: Ajoutez des blocs d'initialisation

Les exemples de constructeurs ci-dessus déclarent simplement des propriétés et leur attribue la valeur d'une expression. Si votre constructeur nécessite plus de code d'initialisation, il peut être placé dans un ou plusieurs blocs init. Dans cette étape, vous allez ajouter des blocs init à la classe Aquarium.

  1. Dans la classe Aquarium, ajoutez un bloc init pour imprimer l'initialisation de l'objet, et un second bloc pour imprimer le volume en litres.
class Aquarium (var length: Int = 100, var width: Int = 20, var height: Int = 40) {
    init {
        println("aquarium initializing")
    }
    init {
        // 1 liter = 1000 cm^3
        println("Volume: ${width * length * height / 1000} l")
    }
}
  1. Exécutez le programme et observez le résultat.
aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 40 cm 
aquarium initializing
Volume: 100 l
Width: 25 cm Length: 100 cm Height: 40 cm 
aquarium initializing
Volume: 77 l
Width: 20 cm Length: 110 cm Height: 35 cm 
aquarium initializing
Volume: 96 l
Width: 25 cm Length: 110 cm Height: 35 cm 

Notez que les blocs init sont exécutés dans l'ordre dans lequel ils apparaissent dans la définition de classe et qu'ils sont exécutés lorsque le constructeur est appelé.

Étape 3: Apprenez à utiliser des constructeurs secondaires

Dans cette étape, vous allez découvrir les constructeurs secondaires et en ajouter un à votre classe. Outre un constructeur principal, qui peut comporter un ou plusieurs blocs init, une classe Kotlin peut également comporter un ou plusieurs constructeurs secondaires pour permettre la surcharge des constructeurs, c'est-à-dire des constructeurs avec différents arguments.

  1. Dans la classe Aquarium, ajoutez un constructeur secondaire qui utilise un certain nombre de poissons comme argument, à l'aide du mot clé constructor. Créez une propriété val pour le réservoir correspondant au volume calculé de l'aquarium en litres, en fonction du nombre de poissons. Supposons que 2 litres (2 000 cm^3) d'eau par poisson, ainsi qu'un peu plus d'espace pour que l'eau ne se déverse pas.
constructor(numberOfFish: Int) : this() {
    // 2,000 cm^3 per fish + extra room so water doesn't spill
    val tank = numberOfFish * 2000 * 1.1
}
  1. Dans le constructeur secondaire, conservez les mêmes valeurs de longueur et de largeur (définies dans le constructeur principal), puis calculez la hauteur nécessaire pour faire du réservoir le volume indiqué.
    // calculate the height needed
    height = (tank / (length * width)).toInt()
  1. Dans la fonction buildAquarium(), ajoutez un appel pour créer un Aquarium à l'aide de votre nouveau constructeur secondaire. Imprimez la taille et le volume.
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} l")
}
  1. Exécutez votre programme et observez le résultat.
⇒ aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

Notez que le volume est imprimé deux fois : une fois par le bloc init dans le constructeur principal avant l'exécution du constructeur secondaire, et une fois par le code dans buildAquarium().

Vous pouvez également inclure le mot clé constructor dans le constructeur principal, mais il n'est pas nécessaire dans la plupart des cas.

Étape 4: Ajoutez un getter de propriété

Dans cette étape, vous allez ajouter un getter de propriété explicite. Kotlin définit automatiquement les getters et les setters lorsque vous définissez des propriétés, mais parfois la valeur d'une propriété doit être ajustée ou calculée. Par exemple, ci-dessus, vous avez imprimé le volume de Aquarium. Vous pouvez rendre le volume disponible en tant que propriété en définissant une variable et un getter pour celle-ci. Étant donné que volume doit être calculé, la méthode "getter" doit renvoyer la valeur calculée, avec une fonction sur une ligne.

  1. Dans la classe Aquarium, définissez une propriété Int appelée volume ainsi qu'une méthode get() qui calcule le volume à la ligne suivante.
val volume: Int
    get() = width * height * length / 1000  // 1000 cm^3 = 1 l
  1. Supprimez le bloc init qui affiche le volume.
  2. Supprimez le code de buildAquarium() qui affiche le volume.
  3. Dans la méthode printSize(), ajoutez une ligne pour imprimer le volume.
fun printSize() {
    println("Width: $width cm " +
            "Length: $length cm " +
            "Height: $height cm "
    )
    // 1 l = 1000 cm^3
    println("Volume: $volume l")
}
  1. Exécutez votre programme et observez le résultat.
⇒ aquarium initializing
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

Les dimensions et le volume sont les mêmes qu'auparavant, mais le volume n'est imprimé qu'une fois après l'initialisation complète de l'objet par le constructeur principal et le constructeur secondaire.

Étape 5: Ajoutez un setter de propriété

Au cours de cette étape, vous allez créer un setter de propriété pour le volume.

  1. Dans la classe Aquarium, remplacez volume par var afin de pouvoir la définir plusieurs fois.
  2. Ajoutez un setter pour la propriété volume en ajoutant une méthode set() sous le getter, qui recalcule la hauteur en fonction de la quantité d'eau fournie. Par convention, le nom du paramètre setter est value, mais vous pouvez le modifier si vous préférez.
var volume: Int
    get() = width * height * length / 1000
    set(value) {
        height = (value * 1000) / (width * length)
    }
  1. Dans buildAquarium(), ajoutez du code pour régler le volume de l'aquarium à 70 litres. Imprimez le nouveau format.
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    aquarium6.volume = 70
    aquarium6.printSize()
}
  1. Exécutez à nouveau votre programme et observez la hauteur et le volume modifiés.
⇒ aquarium initialized
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l
Width: 20 cm Length: 100 cm Height: 35 cm 
Volume: 70 l

Le code ne comporte aucun modificateur de visibilité (par exemple, public ou private) pour l'instant. C'est parce que, par défaut, tout en Kotlin est public, ce qui signifie que tout est accessible partout, y compris les classes, les méthodes, les propriétés et les variables de membre.

Dans Kotlin, les classes, les objets, les interfaces, les constructeurs, les fonctions, les propriétés et leurs setters peuvent avoir des modificateurs de visibilité:

  • public signifie visible en dehors de la classe. Tout est public par défaut, y compris les variables et méthodes de la classe.
  • internal signifie qu'il n'est visible que dans ce module. Un module est un ensemble de fichiers Kotlin compilés, par exemple une bibliothèque ou une application.
  • private signifie qu'il n'est visible que dans cette classe (ou dans le fichier source si vous utilisez des fonctions).
  • protected est identique à private, mais il sera également visible par toutes les sous-classes.

Pour en savoir plus, consultez la section Modificateurs de visibilité dans la documentation Kotlin.

Variables des membres

Les propriétés d'une classe ou des variables de membre sont public par défaut. Si vous les définissez avec var, ils peuvent être modifiés, c'est-à-dire lisibles et accessibles en écriture. Si vous les définissez avec val, ils sont en lecture seule après l'initialisation.

Si vous souhaitez une propriété que votre code peut lire ou écrire, mais que le code externe ne peut lire que, vous pouvez laisser la propriété et son getter publics, et déclarer le setter privé, comme illustré ci-dessous.

var volume: Int
    get() = width * height * length / 1000
    private set(value) {
        height = (value * 1000) / (width * length)
    }

Dans cette tâche, vous allez découvrir le fonctionnement des sous-classes et de l'héritage en langage Kotlin. Elles sont semblables à ce que vous voyez dans d'autres langues, mais il y a quelques différences.

En langage Kotlin, les classes ne peuvent pas être sous-classées. De même, les propriétés et variables de membre ne peuvent pas être remplacées par des sous-classes (mais elles sont accessibles).

Vous devez marquer une classe comme open pour autoriser sa sous-classification. De même, vous devez marquer les propriétés et les variables de membre comme open afin de les remplacer dans la sous-classe. Le mot clé open est obligatoire pour éviter toute divulgation accidentelle d'informations d'implémentation dans l'interface du cours.

Étape 1: Ouvrez le cours de l'aquarium

Dans cette étape, vous allez définir la classe Aquarium open afin de la remplacer à l'étape suivante.

  1. Marquez la classe Aquarium et toutes ses propriétés à l'aide du mot clé open.
open class Aquarium (open var length: Int = 100, open var width: Int = 20, open var height: Int = 40) {
    open var volume: Int
        get() = width * height * length / 1000
        set(value) {
            height = (value * 1000) / (width * length)
        }
  1. Ajoutez une propriété shape ouverte avec la valeur "rectangle".
   open val shape = "rectangle"
  1. Ajoutez une propriété water ouverte avec un getter qui renvoie 90% du volume de Aquarium.
    open var water: Double = 0.0
        get() = volume * 0.9
  1. Ajoutez le code à la méthode printSize() pour imprimer la forme et la quantité d'eau sous forme de pourcentage du volume.
fun printSize() {
    println(shape)
    println("Width: $width cm " +
            "Length: $length cm " +
            "Height: $height cm ")
    // 1 l = 1000 cm^3
    println("Volume: $volume l Water: $water l (${water/volume*100.0}% full)")
}
  1. Dans buildAquarium(), modifiez le code pour créer un Aquarium avec width = 25, length = 25 et height = 40.
fun buildAquarium() {
    val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
    aquarium6.printSize()
}
  1. Exécutez votre programme et observez le nouveau résultat.
⇒ aquarium initializing
rectangle
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 25 l Water: 22.5 l (90.0% full)

Étape 2: Créez une sous-classe

  1. Créez une sous-classe de Aquarium appelée TowerTank, qui implémente un réservoir cylindrique arrondi au lieu d'un réservoir rectangulaire. Vous pouvez ajouter TowerTank sous Aquarium, car vous pouvez ajouter une autre classe dans le même fichier que la classe Aquarium.
  2. Dans TowerTank, ignorez la propriété height, définie dans le constructeur. Pour ignorer une propriété, utilisez le mot clé override dans la sous-classe.
  1. Définissez le constructeur de TowerTank sur diameter. Utilisez diameter pour length et width lorsque vous appelez le constructeur dans la super-classe Aquarium.
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
  1. Ignorez la propriété de volume pour calculer un cylindre. La formule d'un cylindre est Pi multipliée par le carré du rayon multiplié par sa hauteur. Vous devez importer la constante PI depuis java.lang.Math.
    override var volume: Int
    // ellipse area = π * r1 * r2
    get() = (width/2 * length/2 * height / 1000 * PI).toInt()
    set(value) {
        height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
    }
  1. Dans TowerTank, remplacez la propriété water par 80% du volume.
override var water = volume * 0.8
  1. Remplacez shape par "cylinder".
override val shape = "cylinder"
  1. Votre classe TowerTank finale devrait ressembler à l'exemple de code ci-dessous.

Aquarium.kt :

package example.myapp

import java.lang.Math.PI

... // existing Aquarium class

class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
    override var volume: Int
    // ellipse area = π * r1 * r2
    get() = (width/2 * length/2 * height / 1000 * PI).toInt()
    set(value) {
        height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
    }

    override var water = volume * 0.8
    override val shape = "cylinder"
}
  1. Dans buildAquarium(), créez un TowerTank d'un diamètre de 25 cm et d'une hauteur de 45 cm. Imprimez le format.

main.kt:

package example.myapp

fun buildAquarium() {
    val myAquarium = Aquarium(width = 25, length = 25, height = 40)
    myAquarium.printSize()
    val myTower = TowerTank(diameter = 25, height = 40)
    myTower.printSize()
}
  1. Exécutez votre programme et observez le résultat.
⇒ aquarium initializing
rectangle
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 25 l Water: 22.5 l (90.0% full)
aquarium initializing
cylinder
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 18 l Water: 14.4 l (80.0% full)

Parfois, vous souhaitez définir des propriétés ou des comportements communs à partager entre certaines classes associées. Kotlin propose deux méthodes pour procéder, les interfaces et les classes abstraites. Dans cette tâche, vous allez créer une classe AquariumFish abstraite pour les propriétés communes à tous les poissons. Vous créez une interface appelée FishAction pour définir un comportement commun à tous les poissons.

  • Ni une classe abstraite, ni une interface ne peuvent être instanciées séparément, ce qui signifie que vous ne pouvez pas créer directement d'objets de ces types.
  • Les classes abstraites ont des constructeurs.
  • Les interfaces ne peuvent pas avoir de logique de constructeur ni stocker d'état.

Étape 1 : Créer une classe abstraite

  1. Sous example.myapp, créez un fichier AquariumFish.kt.
  2. Créez une classe, également appelée AquariumFish, et marquez-la avec abstract.
  3. Ajoutez une propriété String, color, puis marquez-la avec abstract.
package example.myapp

abstract class AquariumFish {
    abstract val color: String
}
  1. Créez deux sous-classes de AquariumFish, Shark et Plecostomus.
  2. La classe color étant abstraite, les sous-classes doivent l'implémenter. Définissez Shark en gris et Plecostomus en or.
class Shark: AquariumFish() {
    override val color = "gray"
}

class Plecostomus: AquariumFish() {
    override val color = "gold"
}
  1. Dans main.kt, créez une fonction makeFish() pour tester vos classes. Instanciez un Shark et un Plecostomus, puis imprimez leur couleur.
  2. Supprimez votre code de test précédent dans main() et ajoutez un appel à makeFish(). Le code doit ressembler à ceci :

main.kt :

package example.myapp

fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()

    println("Shark: ${shark.color}")
    println("Plecostomus: ${pleco.color}")
}

fun main () {
    makeFish()
}
  1. Exécutez votre programme et observez le résultat.
⇒ Shark: gray 
Plecostomus: gold

Le schéma suivant représente les classes Shark et Plecostomus, qui sous-classe de la classe abstraite AquariumFish.

Schéma illustrant la classe abstraite, AquariumFish, et deux sous-classes, Shark et Plecostumus.

Étape 2. Créer une interface

  1. Dans AquariumFish.kt, créez une interface nommée FishAction avec la méthode eat().
interface FishAction  {
    fun eat()
}
  1. Ajoutez FishAction à chacune des sous-classes et implémentez eat() en indiquant ce qu'il fait.
class Shark: AquariumFish(), FishAction {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}

class Plecostomus: AquariumFish(), FishAction {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. Dans la fonction makeFish(), demandez à chaque poisson que vous avez créé de manger quelque chose en appelant eat().
fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()
    println("Shark: ${shark.color}")
    shark.eat()
    println("Plecostomus: ${pleco.color}")
    pleco.eat()
}
  1. Exécutez votre programme et observez le résultat.
⇒ Shark: gray
hunt and eat fish
Plecostomus: gold
eat algae

Le schéma suivant représente la classe Shark et la classe Plecostomus, qui sont toutes les deux composées et implémentent l'interface FishAction.

Quand utiliser des classes abstraites ou des interfaces ?

Les exemples ci-dessus sont simples. Cependant, quand vous disposez de nombreuses classes liées, des classes abstraites et des interfaces peuvent vous aider à garder votre conception plus propre, plus organisée et plus facile à gérer.

Comme indiqué ci-dessus, les classes abstraites peuvent avoir des constructeurs, mais pas les interfaces. En revanche, elles sont très similaires. Alors, quand les utiliser ?

Lorsque vous utilisez des interfaces pour composer une classe, la fonctionnalité de la classe est étendue à l'aide des instances qu'elle contient. La composition a tendance à faciliter la réutilisation et le raisonnement du code plutôt que l'héritage d'une classe abstraite. Vous pouvez également utiliser plusieurs interfaces dans une classe, mais une seule sous-classe d'une classe abstraite.

La composition s'accompagne souvent d'une meilleure encapsulation, d'une coupulation plus faible (interdépendance), d'interfaces plus propres et d'un code plus facile à utiliser. C'est pourquoi il est préférable d'utiliser la composition avec des interfaces. En revanche, l'héritage d'une classe abstraite est généralement adapté à certains problèmes. Vous devez donc privilégier la composition, mais lorsque l'héritage est logique, Kotlin vous le permet également.

  • Utilisez une interface si vous disposez de nombreuses méthodes et d'une ou deux implémentations par défaut, comme dans AquariumAction ci-dessous.
interface AquariumAction {
    fun eat()
    fun jump()
    fun clean()
    fun catchFish()
    fun swim()  {
        println("swim")
    }
}
  • Utilisez une classe abstraite chaque fois que vous ne pouvez pas terminer une classe. Par exemple, si vous revenez à la classe AquariumFish, vous pouvez appliquer la FishAction à toutes les AquariumFish et fournir une implémentation par défaut pour eat sans laisser color abstrait, car il n'y a pas vraiment de couleur par défaut pour le poisson.
interface FishAction  {
    fun eat()
}

abstract class AquariumFish: FishAction {
   abstract val color: String
   override fun eat() = println("yum")
}

La tâche précédente a introduit des classes abstraites, des interfaces et l'idée de la composition. La délégation d'interface est une technique avancée dans laquelle les méthodes d'une interface sont mises en œuvre par un objet d'aide (ou délégué). Il est ensuite utilisé par une classe. Cette technique peut s'avérer utile lorsque vous utilisez une interface dans une série de classes sans lien direct, avec la possibilité d'ajouter la fonctionnalité d'interface nécessaire à une classe d'assistance distincte. Chacune de ces classes utilise une instance de la classe d'assistance pour implémenter la fonctionnalité.

Dans cette tâche, vous allez ajouter des fonctionnalités à une classe à l'aide de la délégation d'interface.

Étape 1: Créer une interface

  1. Dans AquariumFish.kt, supprimez la classe AquariumFish. Au lieu d'hériter de la classe AquariumFish, Plecostomus et Shark vont implémenter des interfaces pour l'action de poisson et pour leur couleur.
  2. Créez une interface FishColor qui définit la couleur sous forme de chaîne.
interface FishColor {
    val color: String
}
  1. Modifiez Plecostomus pour implémenter deux interfaces, FishAction et FishColor. Vous devez remplacer color de FishColor et eat() de FishAction.
class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. Modifiez votre classe Shark pour qu'elle implémente également les deux interfaces FishAction et FishColor au lieu d'hériter de AquariumFish.
class Shark: FishAction, FishColor {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}
  1. Votre code, une fois fini, doit ressembler à ceci:
package example.myapp

interface FishAction {
    fun eat()
}

interface FishColor {
    val color: String
}

class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}

class Shark: FishAction, FishColor {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}

Étape 2: Créer un cours singleton

Vous allez ensuite implémenter la configuration de la partie de délégation en créant une classe d'assistance qui implémente FishColor. Vous créez une classe de base appelée GoldColor qui implémente FishColor. Elle indique simplement que sa couleur est or.

Il n'est pas logique d'utiliser plusieurs instances de GoldColor, car elles ont toutes le même effet. Kotlin vous permet de déclarer une classe dans laquelle vous ne pouvez créer qu'une seule instance de celui-ci en utilisant le mot clé object au lieu de class. Kotlin crée cette instance, et cette instance est référencée par le nom de la classe. Ensuite, tous les autres objets peuvent utiliser cette instance unique : il n'existe aucun moyen de créer d'autres instances de cette classe. Si vous connaissez le modèle singleton, voici comment implémenter des singletons dans Kotlin.

  1. Dans AquariumFish.kt, créez un objet pour GoldColor. Remplacer la couleur.
object GoldColor : FishColor {
   override val color = "gold"
}

Étape 3: Ajoutez une délégation d'interface pour FishColor

Vous pouvez à présent utiliser la délégation d'interface.

  1. Dans AquariumFish.kt, supprimez le forçage de color dans Plecostomus.
  2. Modifiez la classe Plecostomus pour obtenir sa couleur à partir de GoldColor. Pour ce faire, ajoutez by GoldColor à la déclaration de la classe, ce qui crée la délégation. Cela signifie qu'au lieu d'implémenter FishColor, utilisez l'implémentation fournie par GoldColor. Ainsi, chaque fois qu'un utilisateur accède à color, il est délégué à GoldColor.
class Plecostomus:  FishAction, FishColor by GoldColor {
   override fun eat() {
       println("eat algae")
   }
}

Avec la classe telle quelle, tous les plis sont dorés, mais ces poissons sont en fait disponibles en plusieurs couleurs. Pour résoudre ce problème, ajoutez un paramètre constructeur pour la couleur avec GoldColor comme couleur par défaut pour Plecostomus.

  1. Modifiez la classe Plecostomus pour qu'elle utilise fishColor dans son constructeur et définissez sa valeur par défaut sur GoldColor. Remplacez la délégation de by GoldColor par by fishColor.
class Plecostomus(fishColor: FishColor = GoldColor):  FishAction,
       FishColor by fishColor {
   override fun eat() {
       println("eat algae")
   }
}

Étape 4: Ajoutez une délégation d'interface pour FishAction

De la même manière, vous pouvez utiliser la délégation d'interface pour FishAction.

  1. Dans AquariumFish.kt, créez une classe PrintingFishAction qui implémente FishAction, qui prend un String, un food, puis affiche ce que mange le poisson.
class PrintingFishAction(val food: String) : FishAction {
    override fun eat() {
        println(food)
    }
}
  1. Dans la classe Plecostomus, supprimez la fonction de remplacement eat(), car vous la remplacerez par une délégation.
  2. Dans la déclaration de Plecostomus, déléguez FishAction à PrintingFishAction en transmettant "eat algae".
  3. Avec cette délégation, il n'y a pas de code dans le corps de la classe Plecostomus. Supprimez donc le {}, car tous les remplacements sont gérés par la délégation d'interface.
class Plecostomus (fishColor: FishColor = GoldColor):
        FishAction by PrintingFishAction("eat algae"),
        FishColor by fishColor

Le schéma suivant représente les classes Shark et Plecostomus, toutes deux composées des interfaces PrintingFishAction et FishColor, mais déléguant l'implémentation à ces classes.

La délégation d'interface est puissante, et vous devez généralement réfléchir à la façon de l'utiliser chaque fois que vous pouvez utiliser une classe abstraite dans une autre langue. Il vous permet d'utiliser la composition pour appliquer des comportements plutôt que d'avoir à créer de nombreuses sous-classes, chacune spécialisée dans une méthode différente.

Une classe de données est semblable à un struct dans d'autres langues (elle contient principalement des données), mais un objet de classe de données est toujours un objet. Les objets de classe de données Kotlin présentent certains avantages supplémentaires, comme des utilitaires d'impression et de copie. Dans cette tâche, vous allez créer une classe de données simple et en apprendre davantage sur la compatibilité de Kotlin avec les classes de données.

Étape 1: Créer une classe de données

  1. Ajoutez un nouveau package decor sous le package example.myapp pour contenir le nouveau code. Effectuez un clic droit sur example.myapp dans le volet Projet, puis sélectionnez Fichier > Nouveau > Package.
  2. Dans le package, créez une classe appelée Decoration.
package example.myapp.decor

class Decoration {
}
  1. Pour faire de Decoration une classe de données, ajoutez le mot clé data à la déclaration de classe.
  2. Ajoutez une propriété String appelée rocks pour fournir à la classe des données.
data class Decoration(val rocks: String) {
}
  1. Dans le fichier, en dehors de la classe, ajoutez une fonction makeDecorations() pour créer et imprimer une instance de Decoration avec "granite".
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)
}
  1. Ajoutez une fonction main() pour appeler makeDecorations() et exécutez votre programme. Comme vous pouvez le constater, cette sortie est logique, car il s'agit d'une classe de données.
⇒ Decoration(rocks=granite)
  1. Dans makeDecorations(), instanciez deux autres objets Decoration qui sont tous deux &slate, et imprimez-les.
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)

    val decoration2 = Decoration("slate")
    println(decoration2)

    val decoration3 = Decoration("slate")
    println(decoration3)
}
  1. Dans makeDecorations(), ajoutez une instruction d'impression qui affiche le résultat de la comparaison de decoration1 avec decoration2, et une seconde comparant decoration3 avec decoration2. Utilisez la méthode égals() fournie par les classes de données.
    println (decoration1.equals(decoration2))
    println (decoration3.equals(decoration2))
  1. Exécutez votre code.
⇒ Decoration(rocks=granite)
Decoration(rocks=slate)
Decoration(rocks=slate)
false
true

Étape 2. Utiliser la déstructuration

Pour obtenir les propriétés d'un objet de données et les attribuer à des variables, vous pouvez les attribuer un par un, comme ceci.

val rock = decoration.rock
val wood = decoration.wood
val diver = decoration.diver

À la place, vous pouvez créer des variables, une pour chaque propriété, et attribuer l'objet de données au groupe de variables. Kotlin place la valeur de la propriété dans chaque variable.

val (rock, wood, diver) = decoration

C'est ce qu'on appelle la déstructuration. Le nombre de variables doit correspondre au nombre de propriétés. En outre, les variables sont attribuées dans l'ordre dans lequel elles sont déclarées dans la classe. Voici un exemple complet que vous pouvez essayer sur Decoration.kt.

// Here is a data class with 3 properties.
data class Decoration2(val rocks: String, val wood: String, val diver: String){
}

fun makeDecorations() {
    val d5 = Decoration2("crystal", "wood", "diver")
    println(d5)

// Assign all properties to variables.
    val (rock, wood, diver) = d5
    println(rock)
    println(wood)
    println(diver)
}
⇒ Decoration2(rocks=crystal, wood=wood, diver=diver)
crystal
wood
diver

Si vous n'avez pas besoin d'une ou de plusieurs propriétés, vous pouvez les ignorer en utilisant _ au lieu d'un nom de variable, comme indiqué dans le code ci-dessous.

    val (rock, _, diver) = d5

Dans cette tâche, vous allez découvrir certaines classes spéciales en langage Kotlin, dont les suivantes:

  • Cours de singleton
  • Enums
  • Cours scellés

Étape 1: Rappeler les classes singleton

Reprenons l'exemple précédent avec la classe GoldColor.

object GoldColor : FishColor {
   override val color = "gold"
}

Comme chaque instance de GoldColor effectue la même opération, elle est déclarée en tant que object au lieu de class en tant que singleton. Il ne peut y en avoir qu'une seule.

Étape 2: création d'une énumération

Kotlin prend également en charge les énumérations, ce qui vous permet d'énumérer des éléments et de les mentionner par nom, comme dans d'autres langages. Déclarez une énumération en ajoutant le mot clé enum au début de la déclaration. Une déclaration d'énumération de base ne nécessite qu'une liste de noms, mais vous pouvez également définir un ou plusieurs champs associés à chaque nom.

  1. Dans Decoration.kt, testez un exemple d'énumération.
enum class Color(val rgb: Int) {
   RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}

Les énumérations sont un peu comme les singletons : il ne peut y en avoir qu'un et une seule de chaque valeur dans l'énumération. Par exemple, il ne peut y avoir qu'un seul Color.RED, un Color.GREEN et un Color.BLUE. Dans cet exemple, les valeurs RVB sont attribuées à la propriété rgb pour représenter les composants de couleur. Vous pouvez également obtenir la valeur ordinale d'une énumération à l'aide de la propriété ordinal et son nom à l'aide de la propriété name.

  1. Essayez un autre exemple d'énumération.
enum class Direction(val degrees: Int) {
    NORTH(0), SOUTH(180), EAST(90), WEST(270)
}

fun main() {
    println(Direction.EAST.name)
    println(Direction.EAST.ordinal)
    println(Direction.EAST.degrees)
}
⇒ EAST
2
90

Étape 3: Créez une classe scellée

Une classe scellée est une classe qui peut être sous-classée, mais uniquement dans le fichier où elle est déclarée. Si vous essayez de sous-classer la classe dans un autre fichier, une erreur s'affiche.

Étant donné que les classes et les sous-classes se trouvent dans le même fichier, Kotlin connaîtra toutes les sous-classes de manière statique. Autrement dit, au moment de la compilation, le compilateur voit toutes les classes et sous-classes, et sait qu'il s'agit de toutes ces classes. Il peut donc effectuer des vérifications supplémentaires pour vous.

  1. Dans AquariumFish.kt, essayez un exemple de cours scellé, en conservant le thème aquatique.
sealed class Seal
class SeaLion : Seal()
class Walrus : Seal()

fun matchSeal(seal: Seal): String {
   return when(seal) {
       is Walrus -> "walrus"
       is SeaLion -> "sea lion"
   }
}

La classe Seal ne peut pas être sous-classée dans un autre fichier. Si vous souhaitez ajouter d'autres types Seal, vous devez les ajouter dans le même fichier. Les classes scellées constituent ainsi un moyen sûr de représenter un nombre fixe de types. Par exemple, les classes scellées sont idéales pour renvoyer une réussite ou une erreur à partir d'une API réseau.

Cette leçon a abordé de nombreux points. Bien qu'il soit courant de se familiariser avec d'autres langages de programmation orientés objets, Kotlin ajoute quelques fonctionnalités pour que le code soit concis et lisible.

Classes et constructeurs

  • Définissez une classe en langage Kotlin à l'aide de class.
  • Kotlin crée automatiquement des setters et des getters pour les propriétés.
  • Définissez le constructeur principal directement dans la définition de classe. Exemples :
    class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
  • Si un constructeur principal nécessite du code supplémentaire, écrivez-le dans un ou plusieurs blocs init.
  • Une classe peut définir un ou plusieurs constructeurs secondaires à l'aide de constructor, mais le style Kotlin utilise plutôt une fonction de fabrique.

Modificateurs de visibilité et sous-classes

  • Toutes les classes et les fonctions en langage Kotlin sont public par défaut, mais vous pouvez utiliser des modificateurs pour remplacer la visibilité par internal, private ou protected.
  • Pour créer une sous-classe, la classe parente doit être marquée comme open.
  • Pour remplacer des méthodes et des propriétés dans une sous-classe, les méthodes et les propriétés doivent être marquées comme open dans la classe parente.
  • Une classe scellée ne peut être sous-classée que dans le fichier dans lequel elle est définie. Créez une classe scellée en préfixe de la déclaration avec sealed.

Classes de données, singletons et énumérations

  • Créez une classe de données en ajoutant la déclaration avec le préfixe data.
  • La déstructuration est un raccourci permettant d'attribuer les propriétés d'un objet data à des variables distinctes.
  • Créez une classe Singleton en utilisant object au lieu de class.
  • Définissez une énumération à l'aide de enum class.

Classes abstraites, interfaces et délégation

  • Les classes abstraites et les interfaces constituent deux façons de partager un comportement commun entre les classes.
  • Une classe abstraite définit les propriétés et le comportement, mais quitte l'implémentation pour les sous-classes.
  • Une interface définit le comportement et peut fournir des implémentations par défaut pour tout ou partie du comportement.
  • Lorsque vous utilisez des interfaces pour composer une classe, la fonctionnalité de la classe est étendue à l'aide des instances qu'elle contient.
  • La délégation d'interface utilise la composition, mais délègue également l'implémentation aux classes d'interface.
  • La composition est un moyen efficace d'ajouter des fonctionnalités à une classe à l'aide de la délégation d'interface. La composition générale est préférable, mais l'héritage d'une classe abstraite est mieux adapté à certains problèmes.

Documentation Kotlin

Si vous souhaitez en savoir plus sur un sujet de ce cours ou si vous rencontrez des difficultés, https://kotlinlang.org constitue le meilleur point de départ.

Tutoriels Kotlin

Le site Web https://try.kotlinlang.org propose des tutoriels enrichis appelés Kotlin Koans, un interpréteur Web ainsi qu'une documentation de référence complète avec des exemples.

Cours Udacity

Pour afficher le cours Udacity sur ce sujet, consultez Kotlin Bootcamp for Programmers (Formation Kotlin pour les programmeurs).

IntelliJ IDEA

Consultez la documentation sur IntelliJ IDEA sur le site Web de JetBrains.

Cette section répertorie les devoirs possibles pour les élèves qui effectuent cet atelier de programmation dans le cadre d'un cours animé par un enseignant. C'est à l'enseignant de procéder comme suit:

  • Si nécessaire, rendez-les.
  • Communiquez aux élèves comment rendre leurs devoirs.
  • Notez les devoirs.

Les enseignants peuvent utiliser ces suggestions autant qu'ils le souhaitent, et ils n'ont pas besoin d'attribuer les devoirs de leur choix.

Si vous suivez vous-même cet atelier de programmation, n'hésitez pas à utiliser ces devoirs pour tester vos connaissances.

Répondez à ces questions.

Question 1

Les classes ont une méthode spéciale qui sert de plan pour créer des objets de cette classe. Comment s'appelle cette méthode ?

▢ Un compilateur

▢ Instanciateur

▢ Un constructeur

▢ Un plan

Question 2

Parmi les affirmations suivantes concernant les interfaces et les classes abstraites, laquelle n'est PAS correcte ?

▢ Des classes abstraites peuvent avoir des constructeurs.

▢ Des interfaces ne peuvent pas comporter de constructeurs.

▢ Les interfaces et les classes abstraites peuvent être instanciées directement.

▢ Les propriétés abstraites doivent être implémentées par des sous-classes de la classe abstraite.

Question 3

Parmi les propositions suivantes, laquelle n'est PAS un modificateur de visibilité Kotlin pour les propriétés, les méthodes, etc.?

internal

nosubclass

protected

private

Question 4

Tenez compte de la classe de données suivante:
data class Fish(val name: String, val species:String, val colors:String)
Quel élément N'EST PAS un code valide pour créer et déstructurer un objet Fish ?

val (name1, species1, colors1) = Fish("Pat", "Plecostomus", "gold")

val (name2, _, colors2) = Fish("Bitey", "shark", "gray")

val (name3, species3, _) = Fish("Amy", "angelfish", "blue and black stripes")

val (name4, species4, colors4) = Fish("Harry", "halibut")

Question 5

Imaginons que vous soyez propriétaire d'un zoo avec de nombreux animaux dont tous doivent prendre soin. Parmi les propositions suivantes, laquelle ne correspond PAS à une mise en œuvre d'une prestation de soins ?

interface pour différents types d'aliments consommés par les animaux.

▢ Une classe abstract Caretaker à partir de laquelle vous pouvez créer différents types d'employés.

interface pour fournir de l'eau propre à un animal.

▢ Une classe data pour une entrée dans une planification d'alimentation.

Passez à la leçon suivante : 5.1 Extensions.

Pour une présentation du cours, y compris des liens vers d'autres ateliers de programmation, consultez le "Kotlin Bootcamp for Programmers: Welcome to the course."