Kotlin : Classe, Propriété, Fonction

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

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 Beach, 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à Boat
  3. Dans la classe Boat, définissez et initialisez les variables length, width, floor
  4.     
        package agua
    
        class Boat {
            var length: Int = 10
            var width: Int = 5
            var floor: Int = 1
        }
        
        

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 buildBoat(), elle créé une instance de Boat
  4.     
        package agua
    
        fun buildBoat() {
            val myBoat = Boat()
        }
        
        
    Note : Pour créer une instance, référencez la classe comme s'il s'agissait d'une fonction (Boat()) 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 buildBoat()
  6.     
        package agua
        ...
        fun main() {
            buildBoat()
        }
        
        
  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 Boat, ajoutez une méthode d'affichage :
  2.     
        fun printSize() {
            println("Length: $length m " +
                    "Width: $width m " +
                    "Floor: $floor m ")
        }
        
        
  3. Dans le fichier Main.kt, depuis buildBoat(), appelez la méthode printSize() sur myBoat
  4.     
        fun buildBoat() {
            val myBoat = Boat()
            myBoat.printSize()
        }
        
        
  5. Exécutez, observez le résultat dans la console
  6. Dans buildBoat(), modifiez un des attributs de myBoat, puis affichez le changement
  7.     
        fun buildBoat() {
            val myBoat = Boat()
            myBoat.printSize
            // modification de la hauteur
            myBoat.floor = 60
            myBoat.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 Boat, sans constructeur, avec des propriétés par défaut.
À présent, nous souhaitons avoir la possibilité de créer une instance de bateau avec des dimensions personnalisées.
  1. Dans la classe Boat, 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 Boat(length: Int = 10, width: Int = 5, floor: Int = 1) {
           // Dimensions en cm
           var length: Int = length
           var width: Int = width
           var floor: Int = floor
        ...
        }
        
        
  3. Il est possible d'épurer ce code en kotlin en définissant les propriétés directement dans le constructeur :
  4.     
        class Boat(var length: Int = 10, var width: Int = 5, var floor: Int = 1) {
        ...
        }
        
        
  5. Testez le nommage des paramètres dans le fichier Main.kt, buildBoat(), en créant différentes instances de bateau :
  6.     
        val Boat1 = Boat()
        boat1.printSize()
        // default floor and length
        val Boat2 = Boat(width = 25)
        boat2.printSize()
        // default width
        val boat3 = Boat(floor = 35, length = 110)
        boat3.printSize()
        // everything custom
        val boat4 = Boat(width = 25, floor = 35, length = 110)
        boat4.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 Boat, placez un bloc init :
  2.     
        class Boat(var length: Int = 10, var width: Int = 5, var floor: Int = 1) {
          init {
              println("Boat in construction")
          }
        }
        
        
  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 Boat, ajoutez un constructeur secondaire :
  2.     
        constructor(numberOfPeople: Int) : this() {
        // nombre de personne pouvant être accueillie
        val area = numberOfPeople * 2
        }
        
        
  3. Dans ce constructeur secondaire, ajoutez la modification des attributs :
  4.     
        // la largueur du bateau est fixe, la hauteur est flexible,
        // il s'agit de calculer le nombre d'étage nécessaire pour le bien être des passagers
        floor = area / (length * width)
        
        
  5. Dans le fichier Main.kt, appellez le constructeur ainsi créé depuis buildBoat() :
  6.     
        fun buildBoat() {
        val boat6 = Boat(numberOfFish = 29)
        boat6.printSize()
        println("Area: ${boat6.width * boat6.length * boat6.floor} m2")
        }
        
        
  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 Boat, définissez une propriété personnalisée :
  2.     
        // la superficie se calcule à partir des autres propriétés longueur et largueur
        val area: Int
          get() = length * width * floor
        
        
  3. Enlevez le bloc init affichant la superficie
  4. Enlevez le code lié à l'affichage, dans la fonction buildBoat()
  5. Dans la méthode printSize(), ajoutez l'affichage de la superficie
  6.     
        fun printSize() {
          println("Length: $length m " +
                  "Width: $width m " +
                  "Floor: $floor ")
          println("Area: $area m2")
        }
        
        
  7. Exécutez le programme de nouveau et observez le résultat attendu
Pour notre bateau, nous souhaitons lui spécifier une superficie différente et donc recalculer le nombre d'étage en conséquence.
  1. Dans la classe Boat, changez la propriété area immuable en mutable var
  2. Définissez le mutateur de cette propriété :
  3.     
        var area: Int
          get() = length * width * floor
          set(value) {
                floor = (value) / (length * width)
            }
        
        
  4. Dans buildBoat(), ajoutez la modification de la superficie
  5.     
        fun buildBoat() {
          val boat6 = Boat(numberOfPeople = 29)
          boat6.printSize()
          boat6.area = 70
          boat6.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 le bateau en une classe mère.

  1. Dans la classe boat, 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 var area: Int
            get() = length * width * floor
            set(value) {
                floor = value / (length * width)
            }
        
        
  4. Ajoutez une propriété pour le matériau du bateau
  5.     
        open val material = "wood"
        
        
  6. Ajoutez une propriété pour l'espace réservé aux voyageurs
  7.     
        open var areaForPeople: Double = 0.0
            get() = area * 0.5
        
        
  8. Modifiez la méthode d'affichage pour rendre compte des nouvelles propriétés
  9.     
        fun printSize() {
            println("The boat is in $material (material)")
            println("Length: $length m " +
                    "Width: $width m " +
                    "Floor: $floor ")
            println("Area: $area m2; Area for People: $areaForPeople (${areaForPeople / area * 100.0} % full)")
        }
        
        
  10. Dans buildBoat(), créez une instance de bateau comme suit :
  11.     
        fun buildBoat() {
            val boat6 = Boat(length = 25, width = 25, floor = 40)
            boat6.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 : Le bateau, pouvant également représenter un navire, est trop général, nous souhaitons à présent créer un voilier.

  1. Dans le fichier de la classe Boat, à la suite de la classe Boat, déclarez une classe SailBoat
  2.     
        class SailBoat(override var length: Int, override var width: Int) :
            Boat(length = length, width = width, floor = 2)
        
        
  3. Surchargez, override, la propriété de la superficie, par rapport à la forme du voilier :
  4.     
        override var area: Int
            // area = width * length / 3
            get() = length * width / 3 * floor
            set(value) {
                floor = value / length / width
            }
        
        
  5. Surchargez la propriété de l'espace réservé aux voyageurs
  6.     
        override var areaForPeople = area * 0.25
        
        
  7. Surchargez la propriété du matériau
  8.     
        override val material = "wood"
        
        
  9. Le code complet est :
  10.     
    package agua
    
    class SailBoat(override var length: Int, override var width: Int) :
            Boat(length = length, width = width, floor = 2) {
        override var area: Int
            // area = width * length / 3
            get() = length * width / 3 * floor
            set(value) {
                floor = value / length / width
            }
    
        override var areaForPeople = area * 0.25
    
        override val material = "wood"
    }
        
        
  11. Dans buildBoat(), créez une instance de voilier :
  12.     
        fun buildBoat() {
          val myBoat = Boat(width = 25, length = 25, floor = 40)
          myBoat.printSize()
          val sail = SailBoat(width = 25, length = 25)
          sail.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 Fish.kt
  2. Créez une classe Fish déclarez là comme abstraite :
  3.   
      package agua
    
      abstract class Fish
      
      
  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 Fish : Shark et Plecostomus
  7. Comme la propriété color est abstraite, Shark et Plecostomus doivent la définir
  8.   
      class Shark: Fish() {
          override val color = "grey"
      }
    
      class Plecostomus: Fish() {
          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 Fish.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: Fish(), FishAction {
          override val color = "grey"
          override fun eat() {
              println("hunt and eat fish")
          }
      }
    
      class Plecostomus: Fish(), 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 Fish.kt, modifiez la classe abstraite pour générer une implémentation générale pour le comportement du poisson
  2.     
        abstract class Fish : 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 : Fish() {
          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 le singleton (companion object).

  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