Formation Kotlin pour les programmeurs 4 : programmation orientée objet

Cet atelier de programmation fait partie du cours Kotlin Bootcamp for Programmers. Vous tirerez pleinement parti de ce cours en suivant les ateliers de programmation dans l'ordre. En fonction de vos connaissances, vous pourrez peut-être survoler certaines sections. Ce cours s'adresse aux programmeurs qui connaissent un langage orienté objet et qui souhaitent apprendre Kotlin.

Introduction

Dans cet atelier de programmation, vous allez créer un programme Kotlin et découvrir les classes et les objets dans ce langage. Une grande partie de ce contenu vous sera familière si vous connaissez un autre langage orienté objet, mais Kotlin présente des différences importantes pour réduire la quantité de code que vous devez écrire. Vous découvrirez également les classes abstraites et la délégation d'interface.

Plutôt que de créer une seule application exemple, les leçons de ce cours sont conçues pour développer vos connaissances, mais sont semi-indépendantes les unes des autres afin que vous puissiez parcourir les sections que vous connaissez. Pour les relier, de nombreux exemples utilisent un thème d'aquarium. Si vous souhaitez en savoir plus sur l'histoire de l'aquarium, consultez le cours Udacity Kotlin Bootcamp for Programmers.

Ce que vous devez déjà savoir

  • Les bases de Kotlin, y compris les types, les opérateurs et les boucles
  • Syntaxe des fonctions Kotlin
  • Principes de base de la programmation orientée objet
  • Les bases d'un IDE tel qu'IntelliJ IDEA ou Android Studio

Points abordés

  • Créer des classes et accéder à des propriétés en Kotlin
  • Créer et utiliser des constructeurs de classe en Kotlin
  • Créer une sous-classe et comprendre le fonctionnement de l'héritage
  • À 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 pour les objets. Par exemple, une classe Aquarium est le plan permettant de créer un objet aquarium.
  • Les objets sont des instances de classes. Un objet aquarium est un Aquarium réel.
  • Les propriétés sont des caractéristiques des classes, telles que la longueur, la largeur et la hauteur d'un Aquarium.
  • Les méthodes, également appelées fonctions membres, sont les fonctionnalités de la classe. Les méthodes correspondent à 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 commun à d'autres objets que les aquariums, et il se fait généralement de manière 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é pour l'organiser ou créer une bibliothèque de code. Une fois un package créé, vous pouvez importer son contenu dans un autre fichier et réutiliser le code et les classes qu'il contient.

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

Étape 1 : Créez un package

Les packages peuvent vous aider à 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 Nouveau > Package et nommez-le example.myapp.

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

Les classes sont définies avec le mot clé class, et les noms de classe commencent par convention par une 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 des 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 donc accéder directement aux propriétés, par exemple myAquarium.length.

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

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

  1. Dans le volet Project (Projet) à gauche, effectuez 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 Type, conservez la sélection Fichier et nommez le fichier main.kt. IntelliJ IDEA inclut le nom du package, mais pas la définition de classe pour un fichier.
  4. Définissez une fonction buildAquarium() et créez une instance de Aquarium à l'intérieur. 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 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 dimension de l'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 imprimer les propriétés de dimension modifiées.
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 à travailler avec les propriétés.

Étape 1 : Créer un constructeur

Dans cette étape, vous allez ajouter un constructeur à la classe Aquarium que vous avez créée dans 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 la dimension créée en définissant les propriétés, mais il serait plus simple de la créer directement à la bonne taille.

Dans certains langages de programmation, le constructeur est défini en créant une méthode dans la classe qui porte le même nom que la classe. En Kotlin, vous définissez le constructeur directement dans la déclaration de classe elle-même, en spécifiant les paramètres entre parenthèses comme si la classe était une méthode. Comme pour 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 de constructeur avec des valeurs par défaut pour length, width et height, et 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 méthode Kotlin plus compacte consiste à définir les propriétés directement avec le constructeur, à l'aide de var ou val. Kotlin crée également automatiquement les getters et les 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, ou n'en spécifier que certains, ou encore tous les spécifier et créer un Aquarium de taille entièrement personnalisée. Dans la fonction buildAquarium(), essayez différentes façons 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 eu à surcharger le constructeur ni à écrire une version différente pour chacun de ces cas (plus quelques autres pour les autres combinaisons). Kotlin crée ce qui est nécessaire à partir des valeurs par défaut et des paramètres nommés.

Étape 2 : Ajoutez des blocs d'initialisation

Les constructeurs d'exemple ci-dessus ne font que déclarer des propriétés et leur attribuer la valeur d'une expression. Si votre constructeur a besoin de 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 indiquer que l'objet est en cours d'initialisation, 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 la classe, et qu'ils sont tous exécutés lorsque le constructeur est appelé.

Étape 3 : En savoir plus sur les constructeurs secondaires

Dans cette étape, vous allez découvrir les constructeurs secondaires et en ajouter un à votre classe. En plus d'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 de constructeur, c'est-à-dire des constructeurs avec des arguments différents.

  1. Dans la classe Aquarium, ajoutez un constructeur secondaire qui prend un nombre de poissons comme argument, à l'aide du mot clé constructor. Créez une propriété de réservoir val pour le volume calculé de l'aquarium en litres en fonction du nombre de poissons. Comptez 2 litres (2 000 cm³) d'eau par poisson, plus un peu d'espace supplémentaire pour éviter que l'eau ne déborde.
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 la longueur et la largeur (qui ont été définies dans le constructeur principal), et calculez la hauteur nécessaire pour que le réservoir ait 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 auriez pu inclure le mot clé constructor dans le constructeur principal, mais ce n'est pas nécessaire dans la plupart des cas.

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

Au cours de 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 il arrive que la valeur d'une propriété doive ê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 celui-ci. Comme volume doit être calculé, le getter doit renvoyer la valeur calculée, ce que vous pouvez faire avec une fonction sur une seule ligne.

  1. Dans la classe Aquarium, définissez une propriété Int appelée volume, puis définissez une méthode get() qui calcule le volume sur 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 dans 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'avant, mais le volume n'est imprimé qu'une seule fois après l'initialisation complète de l'objet par le constructeur principal et le constructeur secondaire.

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

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

  1. Dans la classe Aquarium, remplacez volume par var afin qu'il puisse être défini 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 du setter est value, mais vous pouvez le modifier si vous le souhaitez.
var volume: Int
    get() = width * height * length / 1000
    set(value) {
        height = (value * 1000) / (width * length)
    }
  1. Dans buildAquarium(), ajoutez du code pour définir le volume de l'aquarium sur 70 litres. Imprimez la nouvelle taille.
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    aquarium6.volume = 70
    aquarium6.printSize()
}
  1. Exécutez à nouveau votre programme et observez la modification de la hauteur et du volume.
⇒ 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

Jusqu'à présent, le code ne contenait aucun modificateur de visibilité, tel que public ou private. En effet, par défaut, tout est public dans Kotlin, ce qui signifie que tout est accessible partout, y compris les classes, les méthodes, les propriétés et les variables membres.

En 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 les méthodes de la classe.
  • internal signifie qu'il ne sera visible que dans ce module. Un module est un ensemble de fichiers Kotlin compilés ensemble, par exemple une bibliothèque ou une application.
  • private signifie qu'il ne sera visible que dans cette classe (ou dans le fichier source si vous travaillez avec 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 membres

Les propriétés d'une classe, ou variables membres, sont public par défaut. Si vous les définissez avec var, elles sont modifiables, c'est-à-dire lisibles et accessibles en écriture. Si vous les définissez avec val, elles seront en lecture seule après l'initialisation.

Si vous souhaitez qu'une propriété puisse être lue ou écrite par votre code, mais uniquement lue par le code externe, vous pouvez laisser la propriété et son getter comme publics et déclarer le setter comme privé, comme indiqué 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 comment fonctionnent les sous-classes et l'héritage en Kotlin. Elles sont similaires à celles que vous avez vues dans d'autres langues, mais il existe quelques différences.

En Kotlin, par défaut, les classes ne peuvent pas être sous-classées. De même, les propriétés et les variables membres ne peuvent pas être remplacées par des sous-classes (bien qu'elles puissent être accessibles).

Vous devez marquer une classe comme open pour qu'elle puisse être sous-classée. De même, vous devez marquer les propriétés et les variables membres comme open pour les remplacer dans la sous-classe. Le mot clé open est obligatoire pour éviter de divulguer accidentellement des détails d'implémentation dans l'interface de la classe.

Étape 1 : Ouvrez le cours Aquarium

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

  1. Marquez la classe Aquarium et toutes ses propriétés avec le 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 du code à la méthode printSize() pour imprimer la forme et la quantité d'eau en 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éer 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, remplacez la propriété height, qui est définie dans le constructeur. Pour remplacer une propriété, utilisez le mot clé override dans la sous-classe.
  1. Faites en sorte que le constructeur de TowerTank prenne un diameter. Utilisez diameter pour length et width lorsque vous appelez le constructeur dans la superclasse Aquarium.
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
  1. Remplacez la propriété de volume pour calculer un cylindre. La formule pour un cylindre est pi fois le rayon au carré fois la 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 au 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 la taille.

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 un comportement ou des propriétés communs à partager entre certaines classes associées. Kotlin propose deux façons de le faire : 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 le comportement commun à tous les poissons.

  • Ni une classe abstraite ni une interface ne peuvent être instanciées seules, ce qui signifie que vous ne pouvez pas créer d'objets de ces types directement.
  • 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, et 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. Comme color est abstrait, les sous-classes doivent l'implémenter. Définissez Shark sur gris et Plecostomus sur 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 affichez la couleur de chacun.
  2. Supprimez le code de test précédent dans main() et ajoutez un appel à makeFish(). Votre code devrait ressembler à l'exemple ci-dessous.

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 sont des sous-classes de la classe abstraite AquariumFish.

Diagramme montrant la classe abstraite AquariumFish et deux sous-classes, Shark et Plecostomus.

Étape 2 : Créer une interface

  1. Dans AquariumFish.kt, créez une interface appelée FishAction avec une méthode eat().
interface FishAction  {
    fun eat()
}
  1. Ajoutez FishAction à chacune des sous-classes et implémentez eat() en lui demandant d'afficher ce que fait le poisson.
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(), faites manger chaque poisson que vous avez créé 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 les classes Shark et Plecostomus, qui sont toutes deux composées de l'interface FishAction et l'implémentent.

Quand utiliser des classes abstraites plutôt que des interfaces ?

Les exemples ci-dessus sont simples, mais lorsque vous avez de nombreuses classes interdépendantes, les classes abstraites et les 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, contrairement aux interfaces. À part cela, elles sont très similaires. Alors, quand utiliser chacun d'eux ?

Lorsque vous utilisez des interfaces pour composer une classe, la fonctionnalité de la classe est étendue par le biais des instances de classe qu'elle contient. La composition a tendance à rendre le code plus facile à réutiliser et à comprendre que l'héritage à partir d'une classe abstraite. De plus, vous pouvez utiliser plusieurs interfaces dans une classe, mais vous ne pouvez créer de sous-classe qu'à partir d'une seule classe abstraite.

La composition permet souvent une meilleure encapsulation, un couplage (interdépendance) plus faible, des interfaces plus claires et un code plus utilisable. Pour ces raisons, la composition avec des interfaces est la conception privilégiée. En revanche, l'héritage d'une classe abstraite a tendance à s'adapter naturellement à certains problèmes. Vous devez donc privilégier la composition, mais Kotlin vous permet également d'utiliser l'héritage lorsque cela est judicieux.

  • Utilisez une interface si vous avez de nombreuses méthodes et 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, en revenant à la classe AquariumFish, vous pouvez faire en sorte que tous les AquariumFish implémentent FishAction et fournir une implémentation par défaut pour eat tout en laissant color abstrait, car il n'y a pas vraiment de couleur par défaut pour les poissons.
interface FishAction  {
    fun eat()
}

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

La tâche précédente a présenté les classes abstraites, les interfaces et l'idée de composition. La délégation d'interface est une technique avancée dans laquelle les méthodes d'une interface sont implémentées par un objet d'assistance (ou délégué), qui est ensuite utilisé par une classe. Cette technique peut être utile lorsque vous utilisez une interface dans une série de classes non liées : vous ajoutez la fonctionnalité d'interface nécessaire à une classe d'assistance distincte, et chacune des classes utilise une instance de la classe d'assistance pour implémenter la fonctionnalité.

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

Étape 1 : Créez 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 du poisson et sa 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 le color de FishColor et le 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 une classe singleton

Ensuite, vous implémentez la configuration de la partie 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 l'or.

Il n'est pas logique de créer plusieurs instances de GoldColor, car elles feraient toutes exactement la même chose. Kotlin vous permet donc de déclarer une classe dont vous ne pouvez créer qu'une seule instance en utilisant le mot clé object au lieu de class. Kotlin créera cette instance, qui sera référencée par le nom de la classe. Tous les autres objets peuvent alors 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 en Kotlin.

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

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

Vous êtes maintenant prêt à utiliser la délégation d'interface.

  1. Dans AquariumFish.kt, supprimez le remplacement de color à partir de Plecostomus.
  2. Modifiez la classe Plecostomus pour obtenir sa couleur à partir de GoldColor. Pour ce faire, ajoutez by GoldColor à la déclaration de classe afin de créer la délégation. Cela signifie qu'au lieu d'implémenter FishColor, vous devez utiliser l'implémentation fournie par GoldColor. Ainsi, chaque fois que color est consulté, il est délégué à GoldColor.
class Plecostomus:  FishAction, FishColor by GoldColor {
   override fun eat() {
       println("eat algae")
   }
}

Dans l'état actuel de la classe, tous les Plecos sont dorés, mais ces poissons existent en réalité dans de nombreuses couleurs. Pour résoudre ce problème, ajoutez un paramètre de constructeur pour la couleur avec GoldColor comme couleur par défaut pour Plecostomus.

  1. Modifiez la classe Plecostomus pour qu'elle accepte un fishColor transmis avec 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 la 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 toute cette délégation, il n'y a pas de code dans le corps de la classe Plecostomus. Supprimez donc {}, car toutes les substitutions sont gérées 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 leur déléguant l'implémentation.

La délégation d'interface est puissante. Vous devez généralement réfléchir à la façon de l'utiliser chaque fois que vous pourriez utiliser une classe abstraite dans une autre langue. Il vous permet d'utiliser la composition pour insérer des comportements, au lieu de nécessiter de nombreuses sous-classes, chacune spécialisée d'une manière différente.

Une classe de données est semblable à un struct dans d'autres langages (elle existe principalement pour contenir des données), mais un objet de classe de données reste un objet. Les objets de classe de données Kotlin présentent des avantages supplémentaires, tels que des utilitaires d'impression et de copie. Dans cette tâche, vous allez créer une classe de données simple et découvrir la compatibilité de Kotlin avec les classes de données.

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

  1. Ajoutez un 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 comme préfixe à la déclaration de classe.
  2. Ajoutez une propriété String appelée rocks pour fournir des données à la classe.
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(), puis exécutez votre programme. Notez la sortie pertinente qui est créée, 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 deuxième qui compare decoration3 avec decoration2. Utilisez la méthode equals() 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 accéder aux propriétés d'un objet de données et les attribuer à des variables, vous pouvez les attribuer une par une, comme ceci.

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

Vous pouvez plutôt 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, un raccourci utile. Le nombre de variables doit correspondre au nombre de propriétés. 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 dans 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 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 des classes à usage spécifique de Kotlin, y compris les suivantes :

  • Classes Singleton
  • Enums
  • Classes scellées

Étape 1 : Rappel sur les classes singleton

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

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

Étant donné que chaque instance de GoldColor fait la même chose, elle est déclarée en tant que object au lieu de class pour en faire un singleton. Il ne peut y avoir qu'une seule instance.

Étape 2 : Créez une énumération

Kotlin est également compatible avec les énumérations, qui vous permettent d'énumérer un élément et de le désigner par son nom, comme dans d'autres langages. Déclarez une énumération en ajoutant le mot clé enum comme préfixe à la déclaration. Une déclaration d'énumération de base n'a besoin que d'une liste de noms, mais vous pouvez également définir un ou plusieurs champs associés à chaque nom.

  1. Dans Decoration.kt, essayez 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'une seule, et une seule de chaque valeur dans l'énumération. Par exemple, il ne peut y avoir qu'un seul Color.RED, un seul Color.GREEN et un seul 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éer une classe scellée

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

Comme 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 les classes, ce qui lui permet d'effectuer des vérifications supplémentaires pour vous.

  1. Dans AquariumFish.kt, essayez un exemple de classe scellée, en gardant 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 donc 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 sujets. Bien que la plupart des éléments devraient vous être familiers si vous avez déjà utilisé d'autres langages de programmation orientés objet, Kotlin ajoute certaines fonctionnalités pour rendre le code concis et lisible.

Classes et constructeurs

  • Définissez une classe en 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 la classe. Exemples :
    class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
  • Si un constructeur principal a besoin de 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 consiste à utiliser une fonction de fabrique à la place.

Modificateurs de visibilité et sous-classes

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

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

  • Créez une classe de données en ajoutant le préfixe data à la déclaration.
  • 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 sont deux façons de partager un comportement commun entre les classes.
  • Une classe abstraite définit des propriétés et un comportement, mais laisse l'implémentation aux sous-classes.
  • Une interface définit un 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 par le biais des instances de classe 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. En général, la composition est préférable, mais l'héritage d'une classe abstraite est plus adapté à certains problèmes.

Documentation Kotlin

Si vous souhaitez en savoir plus sur un sujet abordé dans ce cours ou si vous êtes bloqué, https://kotlinlang.org est le meilleur point de départ.

Tutoriels Kotlin

Le site Web https://try.kotlinlang.org inclut des tutoriels complets appelés Kotlin Koans, un interpréteur Web et un ensemble complet de documentation de référence avec des exemples.

Cours Udacity

Pour consulter le cours Udacity sur ce sujet, reportez-vous à la formation Kotlin pour les programmeurs.

IntelliJ IDEA

La documentation d'IntelliJ IDEA est disponible sur le site Web de JetBrains.

Cette section répertorie les devoirs possibles pour les élèves qui suivent cet atelier de programmation dans le cadre d'un cours animé par un enseignant. Il revient à l'enseignant d'effectuer les opérations suivantes :

  • Attribuer des devoirs si nécessaire
  • Indiquer aux élèves comment rendre leurs devoirs
  • Noter les devoirs

Les enseignants peuvent utiliser ces suggestions autant qu'ils le souhaitent, et ne doivent pas hésiter à attribuer d'autres devoirs aux élèves s'ils le jugent nécessaire.

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

Répondre aux questions suivantes

Question 1

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

▢ Un générateur

▢ Un instantiateur

▢ Un constructeur

▢ Un plan

Question 2

Parmi les affirmations suivantes concernant les interfaces et les classes abstraites, laquelle est FAUSSE ?

▢ Les classes abstraites peuvent avoir des constructeurs.

▢ Les interfaces ne peuvent pas avoir de constructeurs.

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

▢ Les propriétés abstraites doivent être implémentées par les 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

Considérez cette classe de données :
data class Fish(val name: String, val species:String, val colors:String)
Parmi les propositions suivantes, laquelle 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 possédiez un zoo avec de nombreux animaux qui ont tous besoin d'être soignés. Parmi les éléments suivants, lequel ne fait PAS partie de l'implémentation de la protection ?

▢ Un interface pour les différents types d'aliments que mangent les animaux.

▢ Une classe abstract Caretaker à partir de laquelle vous pouvez créer différents types de responsables.

▢ Un interface pour avoir donné de l'eau propre à un animal.

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

Passez à la leçon suivante : 5.1 Extensions

Pour obtenir un aperçu du cours, y compris des liens vers d'autres ateliers de programmation, consultez Formation Kotlin pour les programmeurs : bienvenue dans le cours.