Classes et héritage en Kotlin

1. Avant de commencer

Conditions préalables

  • Vous maîtrisez l'utilisation de Kotlin Playground pour modifier des programmes Kotlin.
  • Les concepts de base de la programmation en Kotlin sont présentés dans le module 1 de ce cours. Il s'agit, plus précisément, du programme main(), des fonctions avec arguments qui renvoient des valeurs, des variables, des types de données et des opérations, ainsi que des instructions if/else.
  • Vous êtes capable de définir une classe en langage Kotlin, de créer une instance d'objet à partir de celle-ci, et d'accéder à ses propriétés et méthodes.

Points abordés

  • Créer un programme Kotlin qui utilise l'héritage pour implémenter une hiérarchie de classes.
  • Étendre une classe, remplacer ses fonctionnalités existantes et en ajouter de nouvelles.
  • Choisir le modificateur de visibilité approprié pour les variables.

Objectifs de l'atelier

  • Programme Kotlin avec différents types de logements implémentés sous la forme d'une hiérarchie de classes.

Ce dont vous avez besoin

2. Qu'est-ce qu'une hiérarchie de classes ?

Il est dans la nature humaine de classer des éléments ayant des propriétés et un comportement similaires en groupes, voire de former une sorte de hiérarchie entre eux. Vous pouvez définir, par exemple, une vaste catégorie regroupant les légumes, dans laquelle une sous-catégorie rassemble un type plus spécifique, comme les légumineuses. Parmi les légumineuses, il peut y avoir des types encore plus spécifiques, comme les pois, les haricots, les lentilles, les pois chiches et le soja.

Cela peut être représenté sous la forme d'une hiérarchie, car les légumineuses contiennent ou héritent de toutes les propriétés des légumes (par exemple, ce sont des plantes et elles sont comestibles). De même, les pois, haricots et lentilles ont tous les propriétés des légumineuses, ainsi que des propriétés uniques.

Voyons maintenant comment représenter cette relation dans un langage de programmation. Si vous définissez Vegetable comme classe en Kotlin, vous pouvez créer Legume en tant qu'enfant ou que sous-classe de la classe Vegetable. Cela signifie que la classe Legume hérite de toutes les propriétés et méthodes de la classe Vegetable (autrement dit, elles y sont également disponibles).

Vous pouvez représenter cela dans un diagramme de hiérarchie de classes, comme illustré ci-dessous. Vous pouvez désigner Vegetable comme parent ou super-classe de la classe Legume.

87e0a5eb0f85042d.png

Vous pouvez continuer à développer la hiérarchie de classes en créant des sous-classes de Legume, telles que Lentil et Chickpea. Legume devient alors un enfant ou une sous-classe de Vegetable, ainsi qu'un parent ou une super-classe de Lentil et Chickpea. Vegetable est la classe racine ou de niveau supérieur (ou encore de base) de cette hiérarchie.

638655b960530d9.png

Héritage dans les classes Android

Bien que vous puissiez écrire du code Kotlin sans utiliser de classes, comme vous l'avez fait dans les ateliers précédents, de nombreuses parties d'Android vous sont proposées sous la forme de classes, avec des activités, des vues et des groupes de vues. Il est donc essentiel de comprendre le concept des hiérarchies de classes pour développer des applications Android. Cela vous permet, en outre, de bénéficier des fonctionnalités fournies par le framework Android.

Par exemple, il existe dans Android une classe View qui représente une zone rectangulaire à l'écran, et qui est chargée du dessin et de la gestion des événements. La classe TextView est une sous-classe de View, ce qui signifie que TextView hérite de toutes les propriétés et fonctionnalités de View, et ajoute une logique spécifique pour présenter du texte à l'utilisateur.

c39a8aaa5b013de8.png

On peut même dire que les classes EditText et Button sont des enfants de la classe TextView. Elles héritent de toutes les propriétés et méthodes des classes TextView et View, et ajoutent en outre leur propre logique. Par exemple, EditText ajoute sa propre fonctionnalité de modification de texte à l'écran.

Au lieu de copier et de coller toute la logique des classes View et TextView dans la classe EditText, EditText peut simplement sous-classer TextView qui, à son tour, sous-classe la classe View. Ensuite, le code de la classe EditText peut se concentrer spécifiquement sur ce qui différencie ce composant d'interface utilisateur des autres vues.

Le diagramme de hiérarchie de classes s'affiche en haut d'une page de documentation d'une classe Android sur le site Web developer.android.com. Si kotlin.Any est indiqué en haut de la hiérarchie, cela est dû au fait que toutes les classes en langage Kotlin possèdent une super-classe commune appelée Any. En savoir plus

1ce2b1646b8064ab.png

Comme vous pouvez le constater, apprendre à utiliser l'héritage entre les classes peut faciliter l'écriture, la réutilisation, la lecture et le test du code.

3. Créer une classe de base

Hiérarchie de classes de logements

Dans cet atelier de programmation, vous allez créer un programme Kotlin qui illustre le fonctionnement des hiérarchies de classes en utilisant des logements avec une surface utile, des étages et des résidents.

Voici un diagramme de la hiérarchie de classes que vous allez créer. À la racine, une classe Dwelling spécifie les propriétés et fonctionnalités qui sont vraies pour tous les logements, comme sur un plan. On trouve ensuite des classes pour une cabane carrée (SquareCabin), une hutte ronde (RoundHut) et une tour ronde (RoundTower), qui est un élément RoundHut avec plusieurs étages.

de1387ca7fc26c81.png

Les classes que vous allez implémenter sont les suivantes :

  • Dwelling : classe de base représentant un abri non spécifique contenant des informations communes à tous les logements.
  • SquareCabin : cabane carrée en bois, avec une aire de plancher carrée.
  • RoundHut : cabane ronde en paille, avec une aire de plancher circulaire ; classe parente de RoundTower.
  • RoundTower : tour ronde en pierre avec une aire de plancher circulaire et plusieurs étages.

Créer une classe Dwelling abstraite

N'importe quelle classe peut être la classe de base d'une hiérarchie de classes ou le parent d'autres classes.

Une classe "abstraite" est une classe qui ne peut pas être instanciée, car elle n'est pas complètement implémentée. On peut la comparer à un croquis. Un croquis contient les idées et les lignes directrices d'un projet, mais ne fournit généralement pas assez d'informations pour la construction. Vous allez utiliser un croquis (classe abstraite) pour créer un plan (classe) à partir duquel vous construirez l'instance d'objet réelle.

L'avantage d'une super-classe est de contenir des propriétés et fonctions communes à toutes ses sous-classes. Si les valeurs des propriétés et les implémentations des fonctions ne sont pas connues, rendez la classe abstraite. Par exemple, Vegetables possède de nombreuses propriétés communes à tous les légumes, mais vous ne pouvez pas créer une instance d'un légume non spécifique, car vous ne connaissez ni sa forme, ni sa couleur. Ainsi, Vegetable est une classe abstraite qui laisse le soin aux sous-classes de déterminer des détails spécifiques sur chaque légume.

La déclaration d'une classe abstraite commence par le mot clé abstract.

Dwelling sera une classe abstraite comme Vegetable. Elle contient des propriétés et des fonctions communes à de nombreux types de logements, mais les valeurs exactes des propriétés et les détails d'implémentation des fonctions ne sont pas connus.

  1. Accédez à Kotlin Playground à l'adresse https://developer.android.com/training/kotlinplayground.
  2. Dans l'éditeur, supprimez println("Hello, world!") dans la fonction main().
  3. Ajoutez ensuite ce code sous la fonction main() pour créer une classe abstract appelée Dwelling.
abstract class Dwelling(){
}

Ajouter une propriété pour les matériaux de construction

Dans la classe Dwelling, vous définissez des éléments qui sont vrais pour tous les logements, même s'ils peuvent varier pour certains d'entre eux. Dans tous les cas, on a recours à des matériaux de construction.

  1. Dans Dwelling, créez une variable buildingMaterial de type String pour représenter le matériau de construction. Étant donné que les matériaux de construction ne changent pas, utilisez val pour en faire une variable non modifiable.
val buildingMaterial: String
  1. Exécutez votre programme. Vous obtenez alors ce message d'erreur.
Property must be initialized or be abstract

Aucune valeur n'est associée à la propriété buildingMaterial. En fait, vous ne pouvez pas lui attribuer une valeur, car un bâtiment non spécifique n'est composé d'aucun élément spécifique. Ainsi, comme l'indique le message d'erreur, vous pouvez faire précéder la déclaration de buildingMaterial du mot clé abstract pour indiquer qu'elle ne sera pas définie ici.

  1. Ajoutez le mot clé abstract à la définition de la variable.
abstract val buildingMaterial: String
  1. Exécutez votre code. Bien qu'il ne fasse rien, il est maintenant compilé sans aucune erreur.
  2. Créez une instance de Dwelling dans la fonction main(), puis exécutez votre code.
val dwelling = Dwelling()
  1. Vous obtenez une erreur, car vous ne pouvez pas créer d'instance de la classe Dwelling abstraite.
Cannot create an instance of an abstract class
  1. Supprimez ce code incorrect.

À ce stade, votre code finalisé se présente comme suit :

abstract class Dwelling(){
    abstract val buildingMaterial: String
}

Ajouter une propriété pour la capacité

Une autre propriété d'un logement est sa capacité, c'est-à-dire le nombre de personnes qui peuvent y vivre.

Tous les logements ont une capacité qui ne varie pas. Cependant, la capacité ne peut pas être définie dans la super-classe Dwelling. Elle doit l'être dans des sous-classes pour des types de logement spécifiques.

  1. Dans Dwelling, ajoutez un entier abstract val appelé capacity.
abstract val capacity: Int

Ajouter une propriété privée pour le nombre de résidents

Tous les logements auront un nombre de residents (qui peut être inférieur ou égal à capacity). Définissez donc la propriété residents dans la super-classe Dwelling pour que toutes les sous-classes en héritent et l'utilisent.

  1. Vous pouvez faire de residents un paramètre qui est transmis au constructeur de la classe Dwelling. La propriété residents est de type var, car le nombre de résidents peut changer après la création de l'instance.
abstract class Dwelling(private var residents: Int) {

Notez que la propriété residents est marquée avec le mot clé private. "Private" est un modificateur de visibilité en langage Kotlin. Il fait en sorte que seule cette classe puisse voir (et utiliser) la propriété residents. Elle n'est accessible depuis aucun autre emplacement de votre programme. Vous pouvez marquer des propriétés ou des méthodes avec le mot clé "Private". Si aucun modificateur de visibilité n'est spécifié, les propriétés et les méthodes sont publiques (public) par défaut, et elles sont accessibles depuis d'autres parties de votre programme. Le nombre de personnes vivant dans un logement est généralement une information privée (contrairement aux informations sur les matériaux de construction ou sur la capacité du bâtiment). C'est donc une décision raisonnable.

En définissant à la fois la capacité (propriété capacity) du logement et le nombre actuel de résidents (propriété residents), vous pouvez créer une fonction hasRoom() pour déterminer s'il y a de la place pour un autre résident. Vous pouvez définir et implémenter la fonction hasRoom() dans la classe Dwelling, car la formule permettant de calculer s'il y a de la place est la même pour tous les logements. Il y a de la place dans un logement (classe Dwelling) si le nombre de residents est inférieur à capacity. La fonction doit renvoyer true ou false en fonction de cette comparaison.

  1. Ajoutez la fonction hasRoom() à la classe Dwelling.
fun hasRoom(): Boolean {
    return residents < capacity
}
  1. Vous pouvez exécuter ce code, et aucune erreur ne doit se produire. Rien de visible n'est effectué pour le moment.

Votre code, une fois terminé, doit ressembler à ceci :

abstract class Dwelling(private var residents: Int) {

   abstract val buildingMaterial: String
   abstract val capacity: Int

   fun hasRoom(): Boolean {
       return residents < capacity
   }
}

4. Créer des sous-classes

Créer une sous-classe SquareCabin

  1. Créez une classe nommée SquareCabin sous la classe Dwelling.
class SquareCabin
  1. Vous devez ensuite indiquer que SquareCabin est lié à Dwelling. Dans votre code, vous souhaitez indiquer que SquareCabin s'étend à partir de Dwelling (ou qu'il s'agit d'une sous-classe de Dwelling), car SquareCabin fournit une implémentation pour les parties abstraites de Dwelling).

Pour indiquer cette relation d'héritage, ajoutez un signe deux-points (:) après le nom de la classe SquareCabin, puis un appel pour initialiser la classe parente Dwelling. N'oubliez pas d'ajouter des parenthèses après le nom de la classe Dwelling.

class SquareCabin : Dwelling()
  1. Lorsque vous effectuez une extension à partir d'une super-classe, vous devez transmettre les paramètres requis attendus par cette super-classe. Dwelling requiert le nombre de residents comme entrée. Vous pouvez transmettre un nombre fixe de résidents, comme 3.
class SquareCabin : Dwelling(3)

Cependant, vous souhaitez que votre programme soit plus flexible et autorise un nombre variable de résidents pour SquareCabins. Vous allez donc définir residents comme paramètre dans la définition de classe SquareCabin. Ne déclarez pas residents comme val,, car vous réutilisez une propriété déjà déclarée dans la classe parente Dwelling.

class SquareCabin(residents: Int) : Dwelling(residents)
  1. Exécutez votre code.
  2. Cela entraîne des erreurs. Voici un aperçu :
Class 'SquareCabin' is not abstract and does not implement abstract base class member public abstract val buildingMaterial: String defined in Dwelling

Lorsque vous déclarez des variables et des fonctions abstraites, vous vous engagez en quelque sorte à leur attribuer des valeurs et des implémentations ultérieurement. Pour une variable, cela signifie que toute sous-classe de cette classe abstraite doit lui attribuer une valeur. Dans le cas d'une fonction, cela signifie que toute sous-classe doit implémenter le corps de la fonction.

Dans la classe Dwelling, vous avez défini une variable abstract buildingMaterial. SquareCabin est une sous-classe de Dwelling. Elle doit donc fournir une valeur pour buildingMaterial. Utilisez le mot clé override pour indiquer que cette propriété a été définie dans une classe parente et qu'elle est sur le point d'être remplacée dans cette classe.

  1. Dans la classe SquareCabin, remplacez (override) la propriété buildingMaterial et affectez-lui la valeur "Wood".
  2. Faites de même pour capacity, en indiquant que six personnes peuvent vivre dans une SquareCabin.
class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

Votre code fini doit se présenter comme suit :

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

Pour tester votre code, créez une instance de SquareCabin dans votre programme.

Utiliser SquareCabin

  1. Insérez une fonction main() vide avant les définitions de classe Dwelling et SquareCabin.
fun main() {

}

abstract class Dwelling(private var residents: Int) {
    ...
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    ...
}
  1. Dans la fonction main(), créez une instance de SquareCabin nommée squareCabin avec six résidents. Ajoutez des instructions d'impression pour les matériaux de construction, la capacité et la fonction hasRoom().
fun main() {
    val squareCabin = SquareCabin(6)

    println("\nSquare Cabin\n============")
    println("Capacity: ${squareCabin.capacity}")
    println("Material: ${squareCabin.buildingMaterial}")
    println("Has room? ${squareCabin.hasRoom()}")
}

Comme vous pouvez le voir, la fonction hasRoom() n'a pas été définie dans la classe SquareCabin, mais elle l'a été dans la classe Dwelling. Comme SquareCabin est une sous-classe de Dwelling, la fonction hasRoom() a été héritée sans frais. La fonction hasRoom() peut maintenant être appelée sur toutes les instances de SquareCabin, comme illustré dans l'extrait de code sous la forme squareCabin.hasRoom().

  1. Exécutez votre code. La sortie suivante devrait être affichée :
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Vous avez créé squareCabin avec 6 résidents, ce qui est égal à capacity. hasRoom() renvoie donc false. Vous pouvez tester l'initialisation de SquareCabin avec un plus petit nombre de residents. Lorsque vous exécuterez à nouveau votre programme, hasRoom() devrait alors renvoyer true.

Utiliser le mot clé "with" pour simplifier votre code

Dans les instructions println(), chaque fois que vous référencez une propriété ou une fonction de squareCabin, vous devez répéter squareCabin.. Cela devient répétitif et peut constituer une source d'erreurs lorsque vous copiez et collez des instructions d'impression.

Lorsque vous utilisez une instance spécifique d'une classe, et que vous devez accéder à plusieurs propriétés et fonctions de cette instance, vous pouvez demander d'effectuer toutes les opérations suivantes avec cet objet d'instance à l'aide d'une instruction with. Commencez par le mot clé with, suivi du nom de l'instance entre parenthèses, puis d'accolades contenant les opérations à effectuer.

with (instanceName) {
    // all operations to do with instanceName
}
  1. Dans la fonction main(), modifiez vos instructions d'impression afin d'utiliser with.
  2. Supprimez squareCabin. dans les instructions d'impression.
with(squareCabin) {
    println("\nSquare Cabin\n============")
    println("Capacity: ${capacity}")
    println("Material: ${buildingMaterial}")
    println("Has room? ${hasRoom()}")
}
  1. Exécutez à nouveau votre code pour vous assurer qu'il fonctionne sans erreur et affiche la même sortie.
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Voici votre code finalisé :

fun main() {
    val squareCabin = SquareCabin(6)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

Créer une sous-classe RoundHut

  1. Comme pour SquareCabin, ajoutez une autre sous-classe, RoundHut, à Dwelling.
  2. Remplacez buildingMaterial et attribuez-lui la valeur "Straw".
  3. Remplacez la valeur capacity et définissez-la sur 4.
class RoundHut(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Straw"
    override val capacity = 4
}
  1. Dans main(), créez une instance de RoundHut avec trois résidents.
val roundHut = RoundHut(3)
  1. Ajoutez le code ci-dessous pour imprimer les informations sur roundHut.
with(roundHut) {
    println("\nRound Hut\n=========")
    println("Material: ${buildingMaterial}")
    println("Capacity: ${capacity}")
    println("Has room? ${hasRoom()}")
}
  1. Exécutez votre code. La sortie pour l'ensemble du programme doit ressembler à ce qui suit :
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true

Vous disposez à présent d'une hiérarchie de classes semblable à celle-ci, avec Dwelling comme classe racine, et SquareCabin et RoundHut comme sous-classes de Dwelling.

c19084f4a83193a0.png

Créer une sous-classe RoundTower

La dernière classe de cette hiérarchie est une tour ronde (Round Tower). Il s'agit d'une sorte de cabane ronde en pierre à plusieurs étages. Vous pouvez donc faire de RoundTower une sous-classe de RoundHut.

  1. Créez une classe RoundTower qui est une sous-classe de RoundHut. Ajoutez le paramètre residents au constructeur de RoundTower, puis transmettez-le au constructeur de la super-classe RoundHut.
  2. Remplacez buildingMaterial par "Stone".
  3. Définissez la valeur capacity sur 4.
class RoundTower(residents: Int) : RoundHut(residents) {
    override val buildingMaterial = "Stone"
    override val capacity = 4
}
  1. Exécutez ce code. Vous obtenez alors un message d'erreur.
This type is final, so it cannot be inherited from

Cette erreur signifie que la classe RoundHut ne peut pas être sous-classée (ou héritée). Par défaut, en langage Kotlin, les classes sont dites "finales" et ne peuvent pas être sous-classées. Vous n'êtes autorisé à hériter que de classes abstract ou de classes marquées avec le mot clé open. Vous devez donc marquer la classe RoundHut avec le mot clé open pour que l'héritage soit possible.

  1. Ajoutez le mot clé open au début de la déclaration RoundHut.
open class RoundHut(residents: Int) : Dwelling(residents) {
   override val buildingMaterial = "Straw"
   override val capacity = 4
}
  1. Dans main(), créez une instance de roundTower et imprimez les informations la concernant.
 val roundTower = RoundTower(4)
with(roundTower) {
    println("\nRound Tower\n==========")
    println("Material: ${buildingMaterial}")
    println("Capacity: ${capacity}")
    println("Has room? ${hasRoom()}")
}

Voici le code complet :

fun main() {
    val squareCabin = SquareCabin(6)
    val roundHut = RoundHut(3)
    val roundTower = RoundTower(4)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

open class RoundHut(residents: Int) : Dwelling(residents) {
   override val buildingMaterial = "Straw"
   override val capacity = 4
}

class RoundTower(residents: Int) : RoundHut(residents) {
    override val buildingMaterial = "Stone"
    override val capacity = 4
}
  1. Exécutez votre code. Il devrait maintenant fonctionner sans erreur et générer la sortie suivante.
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true

Round Tower
==========
Material: Stone
Capacity: 4
Has room? false

Ajouter plusieurs étages à RoundTower

RoundHut est implicitement un bâtiment de plain-pied. Les tours ont généralement plusieurs étages.

Si l'on tient compte de la capacité, plus une tour compte d'étages, plus sa capacité devrait être importante.

Vous pouvez modifier RoundTower pour qu'elle ait plusieurs étages et ajuster sa capacité en fonction du nombre de niveaux.

  1. Mettez à jour le constructeur RoundTower afin qu'il accepte un paramètre entier supplémentaire val floors pour le nombre d'étages. Placez-le après residents. Notez qu'il n'est pas nécessaire de transmettre ce paramètre au constructeur RoundHut parent, car floors est défini ici dans RoundTower, et RoundHut n'a aucun étage (floors).
class RoundTower(
    residents: Int,
    val floors: Int) : RoundHut(residents) {

    ...
}
  1. Exécutez votre code. Une erreur se produit lors de la création de roundTower dans la méthode main(), car vous ne fournissez pas de nombre pour l'argument floors. Vous pouvez ajouter l'argument manquant.

Vous pouvez également ajouter une valeur par défaut pour floors dans la définition de classe de RoundTower, comme indiqué ci-dessous. Ensuite, si aucune valeur n'est transmise au constructeur pour floors, la valeur par défaut peut être utilisée pour créer l'instance d'objet.

  1. Dans votre code, ajoutez = 2 après la déclaration de floors pour lui affecter la valeur par défaut "2".
class RoundTower(
    residents: Int,
    val floors: Int = 2) : RoundHut(residents) {

    ...
}
  1. Exécutez votre code. La compilation doit normalement être effectuée correctement, car RoundTower(4) crée maintenant une instance d'objet RoundTower avec une valeur par défaut de 2 étages.
  2. Dans la classe RoundTower, modifiez l'élément capacity pour le multiplier par le nombre d'étages.
override val capacity = 4 * floors
  1. Exécutez votre code. Comme vous pouvez le constater, la capacité de RoundTower est maintenant de 8 pour 2 étages.

Voici votre code finalisé.

fun main() {

    val squareCabin = SquareCabin(6)
    val roundHut = RoundHut(3)
    val roundTower = RoundTower(4)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

open class RoundHut(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Straw"
    override val capacity = 4
}

class RoundTower(
    residents: Int,
    val floors: Int = 2) : RoundHut(residents) {

    override val buildingMaterial = "Stone"
    override val capacity = 4 * floors
}

5. Modifier les classes de la hiérarchie

Calculer l'aire de plancher

Dans cet exercice, vous allez apprendre à déclarer une fonction abstraite dans une classe abstraite, puis à implémenter ses fonctionnalités dans les sous-classes.

Tous les logements ont une aire de plancher. Cependant, le mode de calcul de cette aire varie en fonction de la forme du logement.

Définir floorArea() dans la classe Dwelling

  1. Commencez par ajouter une fonction floorArea() abstract à la classe Dwelling. Renvoyez un Double. Double est un type de données, tel que String et Int. Il est utilisé pour les nombres à virgule flottante, c'est-à-dire les nombres avec un signe décimal suivi d'une partie fractionnaire, comme 5,8793.
abstract fun floorArea(): Double

Toutes les méthodes abstraites définies dans une classe abstraite doivent être implémentées dans l'une de ses sous-classes. Avant de pouvoir exécuter votre code, vous devez implémenter floorArea() dans les sous-classes.

Implémenter floorArea() pour SquareCabin

Comme pour buildingMaterial et capacity, puisque vous implémentez une fonction abstract définie dans la classe parente, vous devez utiliser le mot clé override.

  1. Dans la classe SquareCabin, commencez par le mot clé override, suivi de l'implémentation réelle de la fonction floorArea(), comme indiqué ci-dessous.
override fun floorArea(): Double {

}
  1. Renvoyez l'aire de plancher calculée. La formule pour calculer l'aire d'un carré est "côté fois côté". Pour connaître l'aire d'un rectangle, on multiplie la longueur par la largeur. Le corps de la fonction sera return length * length.
override fun floorArea(): Double {
    return length * length
}

La longueur n'est pas une variable dans la classe et elle est différente pour chaque instance. Vous pouvez donc l'ajouter en tant que paramètre de constructeur pour la classe SquareCabin.

  1. Modifiez la définition de classe de SquareCabin pour ajouter un paramètre length de type Double. Déclarez la propriété en tant que val, car la longueur d'un bâtiment ne change pas.
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {

Dwelling et, par conséquent, toutes ses sous-classes ont residents comme argument de constructeur. Puisqu'il s'agit du premier argument du constructeur Dwelling, il est recommandé de le définir également comme premier argument de tous les constructeurs de sous-classe et de placer les arguments dans le même ordre dans toutes les définitions de classe. Insérez donc le nouveau paramètre length après le paramètre residents.

  1. Dans main(), mettez à jour la création de l'instance squareCabin. Transmettez 50.0 au constructeur SquareCabin en tant que length.
val squareCabin = SquareCabin(6, 50.0)
  1. Dans l'instruction with pour squareCabin, ajoutez une instruction d'impression pour l'aire de plancher.
println("Floor area: ${floorArea()}")

Votre code ne s'exécutera pas, car vous devez également implémenter floorArea() dans RoundHut.

Implémenter floorArea() pour RoundHut

De la même manière, vous allez implémenter l'aire de plancher pour RoundHut. RoundHut est également une sous-classe directe de Dwelling. Vous devez donc utiliser le mot clé override.

Pour calculer l'aire de plancher d'un logement de forme circulaire, on utilise la formule PI * rayon * rayon.

PI est une valeur mathématique. Elle est définie dans une bibliothèque mathématique. Une bibliothèque est un ensemble prédéfini de fonctions et de valeurs définies en dehors d'un programme, et qu'un programme peut utiliser. Pour utiliser une valeur ou une fonction de bibliothèque, vous devez indiquer au compilateur votre intention de l'utiliser. Pour ce faire, importez la fonction ou la valeur dans votre programme. Pour utiliser PI dans votre programme, vous devez importer kotlin.math.PI.

  1. Importez PI à partir de la bibliothèque mathématique Kotlin. Placez-le en haut du fichier, avant main().
import kotlin.math.PI
  1. Implémentez la fonction floorArea() pour RoundHut.
override fun floorArea(): Double {
    return PI * radius * radius
}

Avertissement : Si vous n'importez pas kotlin.math.PI, une erreur s'affiche. Vous devez donc importer cette bibliothèque avant de l'utiliser. Vous pouvez également écrire la version complète de PI, comme dans kotlin.math.PI * rayon * rayon. Ensuite, l'instruction d'importation n'est pas nécessaire.

  1. Mettez à jour le constructeur RoundHut pour transmettre le radius.
open class RoundHut(
   residents: Int,
   val radius: Double) : Dwelling(residents) {
  1. Dans main(), mettez à jour l'initialisation de roundHut en transmettant un élément radius de 10.0 au constructeur RoundHut.
val roundHut = RoundHut(3, 10.0)
  1. Ajoutez une instruction d'impression dans l'instruction with pour roundHut.
println("Floor area: ${floorArea()}")

Implémenter floorArea() pour RoundTower

L'exécution de votre code échoue et le message d'erreur suivant est affiché :

Error: No value passed for parameter 'radius'

Dans RoundTower, pour que votre programme soit compilé, il n'est pas nécessaire d'implémenter floorArea(), car il est hérité de RoundHut. Cependant, vous devez mettre à jour la définition de la classe RoundTower pour qu'elle ait également le même argument radius que sa classe parente RoundHut.

  1. Modifiez le constructeur de RoundTower pour qu'il accepte également radius. Placez radius après residents et avant floors. Il est recommandé de lister les variables avec des valeurs par défaut à la fin. N'oubliez pas de transmettre radius au constructeur de la classe parente.
class RoundTower(
    residents: Int,
    radius: Double,
    val floors: Int = 2) : RoundHut(residents, radius) {
  1. Mettez à jour l'initialisation de roundTower dans main().
val roundTower = RoundTower(4, 15.5)
  1. Ajoutez également une instruction d'impression qui appelle floorArea().
println("Floor area: ${floorArea()}")
  1. Vous pouvez maintenant exécuter votre code.
  2. Notez que le calcul de RoundTower est incorrect, car il est hérité de RoundHut et ne prend pas en compte le nombre de floors.
  3. Dans RoundTower, indiquez override floorArea() afin de lui attribuer une implémentation différente qui multiplie la surface par le nombre d'étages. Comme vous pouvez le constater, vous pouvez définir une fonction dans une classe abstraite (Dwelling), l'implémenter dans une sous-classe (RoundHut), puis la remplacer à nouveau dans une sous-classe de la sous-classe (RoundTower). C'est le compromis parfait : vous héritez de la fonctionnalité de votre choix et pouvez remplacer celle dont vous n'avez pas besoin.
override fun floorArea(): Double {
    return PI * radius * radius * floors
}

Ce code fonctionne, mais il existe un moyen d'éviter de répéter celui qui figure déjà dans la classe parente RoundHut. Vous pouvez appeler la fonction floorArea() à partir de la classe parente RoundHut pour renvoyer PI * radius * radius. Multipliez ensuite ce résultat par le nombre de floors.

  1. Dans RoundTower, mettez à jour floorArea() de manière à utiliser l'implémentation de super-classe de floorArea(). Utilisez le mot clé super pour appeler la fonction définie dans le parent.
override fun floorArea(): Double {
    return super.floorArea() * floors
}
  1. Exécutez à nouveau votre code. RoundTower génère alors la surface correcte pour plusieurs étages.

Voici votre code finalisé :

import kotlin.math.PI

fun main() {

    val squareCabin = SquareCabin(6, 50.0)
    val roundHut = RoundHut(3, 10.0)
    val roundTower = RoundTower(4, 15.5)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }
 }

abstract class Dwelling(private var residents: Int) {

    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
        return residents < capacity
}

    abstract fun floorArea(): Double
}

class SquareCabin(residents: Int,
    val length: Double) : Dwelling(residents) {

    override val buildingMaterial = "Wood"
    override val capacity = 6

    override fun floorArea(): Double {
       return length * length
    }
}

open class RoundHut(residents: Int,
    val radius: Double) : Dwelling(residents) {

    override val buildingMaterial = "Straw"
    override val capacity = 4

    override fun floorArea(): Double {
        return PI * radius * radius
    }
}

class RoundTower(residents: Int, radius: Double,
    val floors: Int = 2) : RoundHut(residents, radius) {

    override val buildingMaterial = "Stone"
    override val capacity = 4 * floors

    override fun floorArea(): Double {
        return super.floorArea() * floors
    }
}

Un résultat semblable aux lignes suivantes doit s'afficher :

Square Cabin
============
Capacity: 6
Material: Wood
Has room? false
Floor area: 2500.0

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true
Floor area: 314.1592653589793

Round Tower
==========
Material: Stone
Capacity: 8
Has room? true
Floor area: 1509.5352700498956

Autoriser un nouveau résident à obtenir une chambre

Ajoutez la possibilité pour un nouveau résident d'obtenir une chambre avec une fonction getRoom() qui augmente le nombre de résidents d'une unité. Comme cette logique est la même pour tous les logements, vous pouvez implémenter la fonction dans Dwelling, ce qui la rend disponible pour toutes les sous-classes et leurs enfants. Simple et efficace !

Remarques :

  • Utilisez une instruction if qui n'ajoute un résident que s'il reste de la capacité.
  • Imprimez un message pour le résultat.
  • Vous pouvez utiliser residents++ comme raccourci pour residents = residents + 1 afin d'ajouter 1 à la variable residents.
  1. Implémentez la fonction getRoom() dans la classe Dwelling.
fun getRoom() {
    if (capacity > residents) {
        residents++
        println("You got a room!")
    } else {
        println("Sorry, at capacity and no rooms left.")
    }
}
  1. Ajoutez des instructions d'impression au bloc d'instructions with pour roundHut afin d'observer ce qui se passe lorsque getRoom() et hasRoom() sont utilisés ensemble.
println("Has room? ${hasRoom()}")
getRoom()
println("Has room? ${hasRoom()}")
getRoom()

Sortie pour ces instructions d'impression :

Has room? true
You got a room!
Has room? false
Sorry, at capacity and no rooms left.

Pour en savoir plus, consultez le code de solution.

Installer un tapis dans un logement de forme circulaire

Supposons que, pour installer un tapis dans votre RoundHut ou RoundTower, vous deviez connaître la longueur d'un côté. Placez la fonction dans RoundHut pour qu'elle soit disponible pour tous les logements de forme circulaire.

2e328a198a82c793.png

  1. Commencez par importer la fonction sqrt() à partir de la bibliothèque kotlin.math.
import kotlin.math.sqrt
  1. Implémentez la fonction calculateMaxCarpetLength() dans la classe RoundHut. La formule permettant de calculer la longueur du tapis carré pouvant tenir dans un cercle est sqrt(2) * radius. Cela est expliqué dans le schéma ci-dessus.
fun calculateMaxCarpetLength(): Double {

    return sqrt(2.0) * radius
}

Transmettez une valeur Double, 2.0, à la fonction mathématique sqrt(2.0), car le type renvoyé de la fonction est Double et non Integer.

  1. La méthode calculateMaxCarpetLength() peut maintenant être appelée sur les instances RoundHut et RoundTower. Ajoutez des instructions d'impression à roundHut et roundTower dans la fonction main().
println("Carpet Length: ${calculateMaxCarpetLength()}")

Pour en savoir plus, consultez le code de solution.

Félicitations ! Vous avez créé une hiérarchie de classes complète avec des propriétés et des fonctions. Vous disposez à présent de toutes les connaissances nécessaires pour créer des classes encore plus efficaces.

6. Code de solution

Voici le code de solution complet pour cet atelier de programmation, y compris les commentaires.

/**
* Program that implements classes for different kinds of dwellings.
* Shows how to:
* Create class hierarchy, variables and functions with inheritance,
* abstract class, overriding, and private vs. public variables.
*/

import kotlin.math.PI
import kotlin.math.sqrt

fun main() {
   val squareCabin = SquareCabin(6, 50.0)
   val roundHut = RoundHut(3, 10.0)
   val roundTower = RoundTower(4, 15.5)

   with(squareCabin) {
       println("\nSquare Cabin\n============")
       println("Capacity: ${capacity}")
       println("Material: ${buildingMaterial}")
       println("Floor area: ${floorArea()}")
   }

   with(roundHut) {
       println("\nRound Hut\n=========")
       println("Material: ${buildingMaterial}")
       println("Capacity: ${capacity}")
       println("Floor area: ${floorArea()}")
       println("Has room? ${hasRoom()}")
       getRoom()
       println("Has room? ${hasRoom()}")
       getRoom()
       println("Carpet size: ${calculateMaxCarpetLength()}")
   }

   with(roundTower) {
       println("\nRound Tower\n==========")
       println("Material: ${buildingMaterial}")
       println("Capacity: ${capacity}")
       println("Floor area: ${floorArea()}")
       println("Carpet Length: ${calculateMaxCarpetLength()}")
   }
}

/**
* Defines properties common to all dwellings.
* All dwellings have floorspace,
* but its calculation is specific to the subclass.
* Checking and getting a room are implemented here
* because they are the same for all Dwelling subclasses.
*
* @param residents Current number of residents
*/
abstract class Dwelling(private var residents: Int) {
   abstract val buildingMaterial: String
   abstract val capacity: Int

   /**
    * Calculates the floor area of the dwelling.
    * Implemented by subclasses where shape is determined.
    *
    * @return floor area
    */
   abstract fun floorArea(): Double

   /**
    * Checks whether there is room for another resident.
    *
    * @return true if room available, false otherwise
    */
   fun hasRoom(): Boolean {
       return residents < capacity
   }

   /**
    * Compares the capacity to the number of residents and
    * if capacity is larger than number of residents,
    * add resident by increasing the number of residents.
    * Print the result.
    */
   fun getRoom() {
       if (capacity > residents) {
           residents++
           println("You got a room!")
       } else {
           println("Sorry, at capacity and no rooms left.")
       }
   }

   }

/**
* A square cabin dwelling.
*
*  @param residents Current number of residents
*  @param length Length
*/
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {
   override val buildingMaterial = "Wood"
   override val capacity = 6

   /**
    * Calculates floor area for a square dwelling.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return length * length
   }

}

/**
* Dwelling with a circular floorspace
*
* @param residents Current number of residents
* @param radius Radius
*/
open class RoundHut(
       residents: Int, val radius: Double) : Dwelling(residents) {

   override val buildingMaterial = "Straw"
   override val capacity = 4

   /**
    * Calculates floor area for a round dwelling.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return PI * radius * radius
   }

   /**
    *  Calculates the max length for a square carpet
    *  that fits the circular floor.
    *
    * @return length of square carpet
    */
    fun calculateMaxCarpetLength(): Double {
        return sqrt(2.0) * radius
    }

}

/**
* Round tower with multiple stories.
*
* @param residents Current number of residents
* @param radius Radius
* @param floors Number of stories
*/
class RoundTower(
       residents: Int,
       radius: Double,
       val floors: Int = 2) : RoundHut(residents, radius) {

   override val buildingMaterial = "Stone"

   // Capacity depends on the number of floors.
   override val capacity = floors * 4

   /**
    * Calculates the total floor area for a tower dwelling
    * with multiple stories.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return super.floorArea() * floors
   }
}

7. Résumé

Dans cet atelier de programmation, vous avez appris ce qui suit :

  • Créer une hiérarchie de classes, c'est-à-dire une arborescence de classes dans laquelle les enfants héritent des fonctionnalités des classes parentes. Les sous-classes héritent des propriétés et des fonctions.
  • Créer une classe abstract dont les sous-classes doivent encore implémenter certaines fonctionnalités. Dès lors, une classe abstract ne peut pas être instanciée.
  • Créer des sous-classes d'une classe abstract.
  • Utiliser le mot clé override pour remplacer des propriétés et des fonctions dans les sous-classes.
  • Utiliser le mot clé super pour référencer des propriétés et des fonctions dans la classe parente.
  • Rendre une classe open afin de pouvoir la sous-classer.
  • Rendre une propriété private pour qu'elle ne puisse être utilisée que dans la classe.
  • Utiliser la construction with pour effectuer plusieurs appels sur la même instance d'objet.
  • Importer une fonctionnalité à partir de la bibliothèque kotlin.math.

8. En savoir plus