Kotlin : Classe, Propriété, Fonction

Temps : 45, 20, 30, 15 min.
Diffiulté : ***

Cette partie aborde les fondamentaux de la POO avec le langage Kotlin, selon les points suivants :

  1. Déclaration de classe, constructeurs, attributs, fonctions
  2. Héritage
  3. Classe Abstraite et Interface
  4. Classe Data et Enum

1. Déclaration de classe, constructeurs, attributs, fonctions

Package en Kotlin

En programmation, les packages servent à garder le code organisé.

  1. Ouvrez IntelliJ IDE, créez un nouveau projet New Project
  2. Nommez votre projet Aqua, attention à selectionner IntelliJ pour Build System
  3. Sélectionnez New > Package, depuis le clique droit de src > main > kotlin, nommez-le agua

Déclaration de classe

En Kotlin, une classe est défini avec le mot clé class.
Note : Le nom d'une classe commence par une majuscule.

  1. Sélectionnez New > Kotlin File / Class, depuis le clique droit du package agua
  2. Sous Kind, sélectionnez Class et nommez-là Aquarium
  3. Dans la classe Aquarium, définissez et initialisez les variables width, height, length
  4.     
        package agua
    
        class Aquarium {
            var width: Int = 20
            var height: Int = 40
            var length: Int = 100
        }
        
        

Note : Il n'y a pas besoin d'écrire les getters et setters, ils sont implicites. De plus, par défaut tout est publique, d'où l'absence du mot clé public.

Fonction main

En programmation, la fonction main() est le point d'entré d'un programme.
En Kotlin, une fonction est déclaré avec le mot clé fun.

  1. Sélectionnez New > Kotlin File / Class, depuis le clique droit du package agua
  2. Sous Kind, sélectionnez File et nommez-le Main.kt
  3. Définissez la fonction buildAquarium(), elle créé une instance de Aquarium
  4.     
        package agua
    
        fun buildAquarium() {
            val myAquarium = Aquarium()
        }
        
        
    Note : 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 cette dernière, similaire à l'utilisation du mot clé new dans d'autres langages.

  5. Définissez une fonction main(), elle appelle buildAquarium()
  6.     
        package agua
        ...
        fun main() {
            buildAquarium()
        }
        
        
  7. Exécutez la fonction main() (Control + Shift + R)

Fonction membre

Une fonction membre, member function, ou méthode est une fonction pouvant s'appliquer sur un objet spécifique.

  1. Dans la classe Aquarium, ajoutez une méthode d'affichage :
  2.     
        fun printSize() {
            println("Width: $width cm " +
                    "Length: $length cm " +
                    "Height: $height cm ")
        }
        
        
  3. Dans le fichier Main.kt, depuis buildAquarium(), appelez la méthode printSize() sur myAquarium
  4.     
        fun buildAquarium() {
            val myAquarium = Aquarium()
            myAquarium.printSize()
        }
        
        
  5. Exécutez, observez le résultat dans la console
  6. Dans buildAquarium(), modifiez un des attributs de myAquarium, puis affichez le changement
  7.     
        fun buildAquarium() {
            val myAquarium = Aquarium()
            myAquarium.printSize
            // modification de la hauteur
            myAquarium.height = 60
            myAquarium.printSize()
        }
        
        
  8. Exécutez, observez le résultat attendu

Constructeur

En Kotlin, il y a deux types de constructeurs :

Rappel : Nous avons précédemment créé une classe Aquarium, sans constructeur, avec des propriétés par défaut.
À présent, nous souhaitons avoir la possibilité de créer une instance d'aquarium avec des dimensions personnalisées.
  1. Dans la classe Aquarium, changez l'entête de classe pour inclure un constructeur primaire avec des valeurs par défaut, modifiez les propriétés en conséquence :
  2.     
        class Aquarium(length: Int = 100, width: Int = 20, height: Int = 40) {
           // Dimensions en cm
           var length: Int = length
           var width: Int = width
           var height: Int = height
        ...
        }
        
        
  3. Il est possible d'épurer ce code en kotlin en définissant les propriétés directement dans le constructeur :
  4.     
        class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
        ...
        }
        
        
  5. Testez le nommage des paramètres dans le fichier Main.kt, buildAquarium(), en créant différentes instances d'aquarium :
  6.     
        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()
        
        
  7. Exécutez la fonction main() (Control + Shift + R), observez le résultat attendu

Bloc d'initialisation

En Kotlin, il existe le bloc d'initialisation, init.
Il permet de placer du code d'initialisation lorsque le constructeur en a besoin.

  1. Dans la classe Aquarium, placez un bloc init :
  2.     
        class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
          init {
              println("aquarium initializing")
          }
        }
        
        
  3. Exécutez le programme de nouveau et observez le résultat

Constructeur secondaire

En Kotlin, il est possible de déclarer les deux types de constructeur primaire et secondaire dans la même classe. Cela permet de faire de la surcharge de constructeur, constructor overloading, avec des arguments différents.
Lorsqu'il y a plusieurs constructeur, il s'appelle les uns des autres avec le mot clé this (et des arguments null, cf. Challenge : Créer une vue personnalisée).
Note : L'ordre d'exécution des constructeurs et bloc d'initialisation est

  1. primary constructor
  2. init block
  3. secondary constructor
Bonne pratique : Il est préférable de tout centraliser dans un seul constructeur afin d'éviter d'avoir trop de chemin de code, et le risque de ne pas tous les tester.
  1. Dans la classe Aquarium, ajoutez un constructeur secondaire :
  2.     
        constructor(numberOfFish: Int) : this() {
        // tank: le réservoir d'eau, la cuve
        // 2,000 cm^3 par poisson + espace sup. pour éviter les éclaboussures
        val tank = numberOfFish * 2000 * 1.1
        }
        
        
  3. Dans ce constructeur secondaire, ajoutez la modification des attributs :
  4.     
        // la largueur de l'aquarium est fixe, la hauteur est flexible,
        // il s'agit de calculer la hauteur nécessaire pour le bien être des poissons
        height = (tank / (length * width)).toInt()
        
        
  5. Dans le fichier Main.kt, appellez le constructeur ainsi créé depuis buildAquarium() :
  6.     
        fun buildAquarium() {
        val aquarium6 = Aquarium(numberOfFish = 29)
        aquarium6.printSize()
        println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} liters")
        }
        
        
  7. Exécutez le programme de nouveau et observez le résultat

Accesseur et mutateur

En Kotlin, l'accesseur et le mutateur d'une propriété est automatiquement créés. Cependant, il est possible de créer un mutateur, ou un accesseur, personnalisé pour chaque propriété.

  1. Dans la classe Aquarium, définissez une propriété personnalisée :
  2.     
        // le volume se calcule à partir des autres propriétés hauteur et largueur
        val volume: Int
          get() = width * height * length / 1000  // 1000 cm^3 = 1 liter
        
        
  3. Enlevez le bloc init affichant le volume
  4. Enlevez le code lié à l'affichage, dans la fonction buildAquarium()
  5. Dans la méthode printSize(), ajoutez l'affichage du volume
  6.     
        fun printSize() {
          println("Width: $width cm " +
                  "Length: $length cm " +
                  "Height: $height cm "
          )
          // 1 liter = 1000 cm^3
          println("Volume: $volume liters")
        }
        
        
  7. Exécutez le programme de nouveau et observez le résultat attendu
Pour notre aquarium, nous souhaitons lui spécifier une hauteur différente et donc recalculer la hauteur en conséquence.
  1. Dans la classe Aquarium, changez la propriété volume immuable en mutable var
  2. Définissez le mutateur de cette propriété :
  3.     
        // le volume se calcule à partir des autres propriétés hauteur et largueur
        val volume: Int
          get() = width * height * length / 1000  // 1000 cm^3 = 1 liter
          set(value) {
            height = (value * 1000) / (width * length)
        }
        
        
  4. Dans buildAquarium(), ajoutez la modification du volume
  5.     
        fun buildAquarium() {
          val aquarium6 = Aquarium(numberOfFish = 29)
          aquarium6.printSize()
          aquarium6.volume = 70
          aquarium6.printSize()
        }
        
        
  6. Exécutez le programme de nouveau et observez le résultat

Visibilité des membres

En Kotlin, les classes, les objets, les interface, les constructeurs, les fonctions, les propriétés et leurs accesseurs peuvent avoir des modificateurs de visibilité.
Par défaut, tout est publique, public.

public visible au dehors de la classe
private seulement visible à l'intérieur de la classe
protected visible également par les sous classes
internal visible dans le module (c'est un ensemble de classe ou fichier compiler ensemble)

2. Héritage

En Kotlin, l'héritage n'est pas automatique, une classe mère doit être déclaré open, de façon a lui permettre d'être une super classe.
De même les propriétés et membre doivent être déclaré open.

Classe open

L'objectif est de transformer l'aquarium en une classe mère.

  1. Dans la classe Aquarium, changez la signature de façon a déclaré la classe open
  2. Déclarez également toutes les propriétés comme open
  3.     
        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)
            }
        
        
  4. Ajoutez une propriété pour la forme de l'aquarium
  5.     
        open val shape = "rectangle"
        
        
  6. Ajoutez une propriété pour la quantité d'eau
  7.     
        open var water: Double = 0.0
            get() = volume * 0.9
        
        
  8. Modifiez la méthode d'affichage pour rendre compte des nouvelles propriétés
  9.     
        fun printSize() {
            println(shape)
            println("Width: $width cm " +
                    "Length: $length cm " +
                    "Height: $height cm ")
            // 1 l = 1000 cm^3
            println("Volume: $volume liters Water: $water liters (${water / volume * 100.0}% full)")
        }
        
        
  10. Dans buildAquarium(), créez une instance d'aquarium comme suit :
  11.     
        fun buildAquarium() {
            val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
            aquarium6.printSize()
        }
        
        
  12. Exécutez le programme de nouveau et observez le résultat

Sous Classe

Il s'agit de créer une sous classe ou classe fille. Contexte : L'aquarium est rectangle, nous souhaitons à présent créer un aquarium cyclindrique.

  1. Dans le fichier de la classe Aquarium, à la suite de la classe Aquarium, déclarez une classe TowerTank
  2.     
        class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
        
        
  3. Surchargez, override, la propriété du volume, par rapport à la forme de l'aquarium :
  4.     
        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()
       }
        
        
  5. Surchargez la propriété de la quantité d'eau
  6.     
        override var water = volume * 0.8
        
        
  7. Surchargez la propriété de la forme
  8.     
        override val shape = "cylinder"
        
        
  9. Le code complet est :
  10.     
        package agua
    
        import java.lang.Math.PI
    
      ... // Classe Aquarium existante
    
      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"
      }
        
        
  11. Dans buildAquarium(), créez une instance d'aquarium cyclindrique :
  12.     
        fun buildAquarium() {
          val myAquarium = Aquarium(width = 25, length = 25, height = 40)
          myAquarium.printSize()
          val myTower = TowerTank(diameter = 25, height = 40)
          myTower.printSize()
        }
        
        
  13. Exécutez le programme de nouveau et observez le résultat

3. Classe Abstraite et Interface

Dans certain cas, nous souhaitons définir des comportements identiques pour des propriétés ou des classes.
Par exemple, nous allons créer :

Classe abstraite

Une classe abstraite est partiellement définie. C'est de la responsabilité de la sous classe de définir les méthodes et propriétés.
Par défaut une classe abstract est open, il n'y a pas besoin de le spécifier.
Elle peut avoir des propriétés et des méthodes abstraite, dans ce cas la sous classe est en charge de les définir.
Elle peut aussi définir un constructeur commun pour toute les sous classes.

  1. Dans le package agua, créez un nouveau fichier AquariumFish.kt
  2. Créez une classe AquariumFish déclarez là comme abstraite :
  3.   
      package agua
    
      abstract class AquariumFish
      
      
  4. Ajoutez une propriété abstraite dans la classe :
  5.   
      abstract val color: String
      
      
  6. Dans le même fichier, créez deux sous classes de AquariumFish : Shark et Plecostomus
  7. Comme la propriété color est abstraite, Shark et Plecostomus doivent la définir
  8.   
      class Shark: AquariumFish() {
          override val color = "grey"
      }
    
      class Plecostomus: AquariumFish() {
          override val color = "gold"
      }
      
      
  9. Dans le fichier Main.kt, créez une fonction makeFish() afin d'y instancier des poissons
  10.   
      fun makeFish() {
          val shark = Shark()
          val pleco = Plecostomus()
    
          println("Shark: ${shark.color}")
          println("Plecostomus: ${pleco.color}")
      }
      
      
  11. Appelez makeFish() dans le main, exécutez le programme et observez le résultat

Interface

  1. Dans le fichier AquariumFish.kt, créez une interface avec une méthode
  2.     
        interface FishAction  {
            fun eat()
        }
        
        
  3. Ajoutez l'interface FishAction aux deux sous classes
  4. Elles doivent alors implémenter la méthode eat() :
  5.     
        class Shark: AquariumFish(), FishAction {
          override val color = "grey"
          override fun eat() {
              println("hunt and eat fish")
          }
      }
    
      class Plecostomus: AquariumFish(), FishAction {
          override val color = "gold"
          override fun eat() {
              println("eat algae")
          }
      }
        
        
  6. Dans la fonction makeFish(), faites manger les poissons
  7.     
        fun makeFish() {
          val shark = Shark()
          val pleco = Plecostomus()
          println("Shark: ${shark.color}")
          shark.eat()
          println("Plecostomus: ${pleco.color}")
          pleco.eat()
        }
        
        
  8. Exécutez le programme de nouveau et observez le résultat

Classe abstraite Vs Interface

L'astuce est d'utiliser une classe abstraite tant qu'il n'est pas possible de compléter la classe.

  1. Dans le fichier AquariumFish.kt, modifiez la classe abstraite pour générer une implémentation générale pour le comportement du poisson
  2.     
        abstract class AquariumFish : FishAction {
           abstract val color: String
           override fun eat() = println("yum")
        }
        
        
  3. Créez un poisson rouge, une sous classe de cette classe abstraite
  4.     
        class RedFish : AquariumFish() {
          override val color = "red"
        }
        
        
  5. Dans la fonction makeFish() testez le poisson rouge
  6.     
        fun makeFish() {
          val fish = RedFish()
          println("Fish: ${fish.color}")
          fish.eat()
        }
        
        
  7. Exécutez le programme de nouveau et observez le résultat

4. Classe Data et Enum

En Kotlin, les qualificatifs de classe les plus connus sont :

Classe de donnée

La classe de donnée, ou data class, est une bonne pratique afin d'indiquer la simplicité de l'objet.
Déclarée avec le mot clé data, la classe a accès a des fonctions générées automatiquement, tel que la fonction equals(), hascode(), toString()...

  1. Créez un nouveau package dans agua, nommez-le deco
  2. Dans ce package, créez une nouvelle classe, de donnée, Decoration :
  3.     
        data class Decoration(val rocks: String)
        
        
  4. Dans ce fichier Decoration.kt, à l'extérieur de la classe, ajoutez une fonction makeDecorations() pour afficher des instances de la classe de donnée
  5.     
        fun makeDecorations() {
          val decoration1 = Decoration("granite")
          println(decoration1)
        }
        
        
  6. Ajoutez une fonction main() pour tester, exécutez
  7. Ajoutez deux autres instances crystal dans makeDecorations()
  8.     
        fun makeDecorations() {
          val decoration1 = Decoration("granite")
          val d2 = Decoration("crystal")
          val d3 = Decoration("crystal")
    
          println("$decoration1 $d2 $d3")
        }
        
        
  9. Exécutez le programme
  10. Dans makeDecorations(), testez l'égalité des instances :
  11.     
        println("deco 1 et deco 2 : ${decoration1.equals(d2)}")
        println("deco 2 et deco 3 (equals): ${d2.equals(d3)}")
        println("deco 2 et deco 3 (==): ${d2 == d3}")
        println("deco 2 et deco 3 (===): ${d2 === d3}")
        
        

Note : == et equals c'est pareil (operator overloading), === correspond à l'égalité sur la référence de l'objet

Classe d'énumération

La classe d'énumération, ou enum class, est une bonne pratique afin d'énumerer des valeurs.
Cela dit, il est plus courant d'utiliser la classe salée.

  1. Dans le fichier Decoration.kt, déclarez une classe d'énumération :
  2.   
      enum class Direction(val degrees: Int) {
          NORTH(0), SOUTH(180), EAST(90), WEST(270)
      }
      
      
  3. Dans le main testez :
  4.   
      println(Direction.EAST.name)
      println(Direction.EAST.ordinal)
      println(Direction.EAST.degrees)
      
      
  5. Exécutez

Finalement, Kotlin est un langage de programmation fonctionnelle et orientée objet par rapport à :

Référence

developer.android.com: Using Classes and Objects in Kotlin