Utiliser des classes et des objets en Kotlin

1. Avant de commencer

Cet atelier de programmation vous explique comment utiliser des classes et des objets en langage Kotlin.

Les classes fournissent des plans à partir desquels vous pouvez créer des objets. Un objet est une instance d'une classe composée de données spécifiques à cet objet. Les objets et les instances de classe peuvent être utilisés de façon interchangeable.

À titre de comparaison, imaginez que vous construisiez une maison. Une classe est comparable à un plan d'architecte. Le plan n'est pas la maison, mais un ensemble d'instructions sur la façon de la construire. La maison est l'objet qui est construit à partir du plan.

Tout comme le plan d'une maison spécifie plusieurs pièces, ayant chacune sa propre conception et sa propre fonction, chaque classe possède une conception et une fonction qui lui sont propres. Pour apprendre à concevoir vos classes, vous devez vous familiariser avec la programmation orientée objet, un framework qui vous apprend à placer les données, la logique et le comportement dans des objets.

La programmation orientée objet vous aide à simplifier des problèmes concrets complexes en les scindant en objets plus petits. Quatre concepts de base sont associés à ce type de programmation. Vous les découvrirez de manière détaillée dans la suite de cet atelier de programmation :

  • Encapsulation. Encapsule les propriétés et méthodes associées qui effectuent des actions sur ces propriétés dans une classe. Prenons l'exemple de votre téléphone mobile. Il "encapsule" un appareil photo, un écran, des cartes mémoire, ainsi que plusieurs autres composants matériels et logiciels. Vous n'avez pas à vous soucier de la façon dont les composants sont câblés en interne.
  • Abstraction. Il s'agit d'une extension de l'encapsulation. L'idée est de masquer autant que possible la logique d'implémentation interne. Par exemple, pour prendre une photo avec votre téléphone mobile, il vous suffit d'ouvrir l'application Appareil photo, de pointer votre téléphone vers la scène que vous souhaitez capturer, puis de cliquer sur un bouton pour prendre la photo. Nul besoin de savoir comment est construite l'application Appareil photo ni de comprendre comment fonctionne réellement l'appareil photo de votre téléphone mobile. En bref, le mécanisme interne de l'application Appareil photo et la façon dont l'appareil photo mobile capture les photos sont rendus abstraits pour vous permettre d'effectuer les tâches importantes.
  • Héritage. Permet de construire une classe sur la base des caractéristiques et du comportement d'autres classes en établissant une relation parent-enfant. Par exemple, plusieurs fabricants produisent des appareils mobiles équipés de l'OS Android, mais l'interface utilisateur est différente pour chacun d'eux. En d'autres termes, les fabricants héritent de la fonctionnalité de l'OS Android et y ajoutent leurs personnalisations.
  • Polymorphisme. Le mot est une adaptation de la racine grecque poly-, qui signifie "plusieurs", et de -morphisme, qui signifie forme. Le polymorphisme est la capacité à utiliser différents objets d'une façon commune. Par exemple, lorsque vous connectez une enceinte Bluetooth à votre téléphone mobile, la seule chose que le téléphone a besoin de savoir, c'est qu'un appareil peut lire du contenu audio via le Bluetooth. Cependant, vous pouvez faire votre choix parmi toute une gamme d'enceintes Bluetooth et votre téléphone n'a pas besoin de savoir comment utiliser spécifiquement chacune d'elles.

Pour terminer, vous découvrirez les délégués de propriété, qui fournissent du code réutilisable pour gérer des valeurs de propriété avec une syntaxe concise. Dans cet atelier de programmation, vous apprendrez ces concepts en créant une structure de classe pour une application de maison connectée.

Conditions préalables

  • Vous savez comment ouvrir, modifier et exécuter du code dans Kotlin Playground.
  • Vous connaissez les bases de la programmation Kotlin, y compris les variables, les fonctions, ainsi que les fonctions println() et main().

Points abordés

  • Présentation de la programmation orientée objet
  • Présentation des classes
  • Définir une classe avec des constructeurs, des fonctions et des propriétés
  • Instancier un objet
  • Définition de l'héritage
  • Différence entre les relations IS-A et HAS-A
  • Remplacer des propriétés et des fonctions
  • Présentation des modificateurs de visibilité
  • Présentation du délégué et savoir utiliser le délégué by

Objectifs de l'atelier

  • Créer une structure de classe pour la maison connectée
  • Créer des classes représentant des appareils connectés, comme une smart TV et une ampoule connectée

Ce dont vous avez besoin

  • Un ordinateur avec accès à Internet et un navigateur Web

2. Définir une classe

Lorsque vous définissez une classe, vous spécifiez les propriétés et les méthodes que tous les objets de cette classe doivent posséder.

Une définition de classe commence par le mot clé class, suivi d'un nom et d'une série d'accolades. La partie de la syntaxe située avant l'accolade ouvrante est également appelée en-tête de classe. Vous pouvez spécifier les propriétés et les fonctions de la classe entre accolades. Vous en apprendrez bientôt plus sur les propriétés et les fonctions. Le schéma ci-dessous présente la syntaxe d'une définition de classe :

Elle commence par un mot clé de classe, suivi d'un nom, et d'une série d'accolades ouvrantes et fermantes. Les accolades contiennent le corps de la classe qui en décrit le plan.

Voici les conventions d'attribution de noms recommandées pour une classe :

  • Vous pouvez choisir n'importe quel nom de classe, mais n'utilisez pas de mots clés Kotlin, tels que fun.
  • Le nom de la classe est écrit au format PascalCase. Chaque mot commence donc par une lettre majuscule et il n'y a pas d'espace entre les mots, comme dans SmartDevice.

Une classe comprend trois parties principales :

  • Propriétés. Variables spécifiant les attributs des objets de la classe.
  • Méthodes. Fonctions contenant les comportements et les actions de la classe.
  • Constructeurs. Fonction membre spéciale qui crée des instances de la classe dans tout le programme dans lequel elle est définie.

Ce n'est pas la première fois que vous travaillez avec des classes. Dans les précédents ateliers de programmation, vous avez découvert les types de données, tels que Int, Float, String et Double. Ces types de données sont définis comme des classes en langage Kotlin. Lorsque vous définissez une variable comme indiqué dans cet extrait de code, vous créez un objet de la classe Int, qui est instancié avec une valeur 1 :

val number: Int = 1

Définissez une classe SmartDevice :

  1. Dans Kotlin Playground, remplacez le contenu par une fonction main() vide :
fun main() {
}
  1. Sur la ligne qui précède la fonction main(), définissez une classe SmartDevice avec un corps contenant un commentaire // empty body :
class SmartDevice {
    // empty body
}

fun main() {
}

3. Créer une instance d'une classe

Comme nous l'avons vu précédemment, une classe est un plan d'un objet. L'environnement d'exécution Kotlin utilise la classe (ou plan) pour créer un objet de ce type. Avec la classe SmartDevice, vous avez le plan d'un appareil connecté. Pour intégrer un appareil connecté réel à votre programme, vous devez créer une instance d'objet SmartDevice. Comme vous pouvez le voir ci-dessous, la syntaxe d'instanciation commence par le nom de la classe, suivi d'une paire de parenthèses :

1d25bc4f71c31fc9.png

Pour utiliser un objet, vous devez le créer et l'affecter à une variable, de la même manière que vous définissez une variable. Vous utilisez le mot clé val pour créer une variable non modifiable et le mot clé var pour une variable modifiable. Le mot clé val ou var est suivi du nom de la variable, d'un opérateur d'affectation =, puis de l'instanciation de l'objet de classe. La syntaxe est illustrée dans le schéma ci-dessous :

f58430542f2081a9.png

Instanciez la classe SmartDevice en tant qu'objet :

  • Dans la fonction main(), utilisez le mot clé val pour créer une variable nommée smartTvDevice et l'initialiser en tant qu'instance de la classe SmartDevice :
fun main() {
    val smartTvDevice = SmartDevice()
}

4. Définir des méthodes de classe

Dans le module 1, vous avez appris ce qui suit :

  • La définition d'une fonction utilise le mot clé fun, suivi d'une série de parenthèses et d'accolades. Les accolades contiennent du code. Il s'agit des instructions requises pour exécuter une tâche.
  • L'appel d'une fonction entraîne l'exécution de tout le code qu'elle contient.

Les actions que la classe peut effectuer y sont définies en tant que fonctions. Imaginons, par exemple, que vous possédiez un appareil connecté (une smart TV ou un système d'éclairage connecté) que vous pouvez allumer et éteindre à l'aide de votre téléphone mobile. L'appareil connecté est converti en classe SmartDevice en programmation, et l'action permettant de l'allumer ou de l'éteindre est représentée par les fonctions turnOn() et turnOff(), qui activent le comportement "allumé" et "éteint".

La syntaxe permettant de définir une fonction dans une classe est identique à celle décrite précédemment. La seule différence réside dans le fait que la fonction est placée dans le corps de la classe. Lorsque vous définissez une fonction dans le corps de la classe, elle est désignée sous le nom de fonction membre ou méthode. Elle représente le comportement de la classe. Dans la suite de cet atelier de programmation, les fonctions seront appelées "méthodes" chaque fois qu'elles apparaîtront dans le corps d'une classe.

Définissez une méthode turnOn() et turnOff() dans la classe SmartDevice :

  1. Dans le corps de la classe SmartDevice, définissez une méthode turnOn() avec un corps vide :
class SmartDevice {
    fun turnOn() {

    }
}
  1. Dans le corps de la méthode turnOn(), ajoutez une instruction println(), puis transmettez-lui une chaîne "Smart device is turned on." :
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }
}
  1. Après la méthode turnOn(), ajoutez une méthode turnOff() qui imprime une chaîne "Smart device is turned off." :
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}

Appeler une méthode sur un objet

Jusqu'à présent, vous avez défini une classe servant de plan pour un appareil connecté, créé une instance de la classe et affecté cette instance à une variable. Vous allez maintenant utiliser les méthodes de la classe SmartDevice pour allumer et éteindre l'appareil.

Pour appeler une méthode dans une classe, la procédure est la même que celle utilisée pour appeler d'autres fonctions à partir de la fonction main() dans l'atelier de programmation précédent. Par exemple, si vous devez appeler la méthode turnOff() à partir de turnOn(), vous pouvez écrire un extrait de code semblable à celui-ci :

class SmartDevice {
    fun turnOn() {
        // A valid use case to call the turnOff() method could be to turn off the TV when available power doesn't meet the requirement.
        turnOff()
        ...
    }

    ...
}

Pour appeler une méthode de classe en dehors de la classe, commencez par l'objet de classe suivi de l'opérateur ., du nom de la fonction et d'une paire de parenthèses. Le cas échéant, les parenthèses contiennent les arguments requis par la méthode. La syntaxe est illustrée dans le schéma ci-dessous :

fc609c15952551ce.png

Appelez les méthodes turnOn() et turnOff() sur l'objet :

  1. Appelez la méthode turnOn() dans la fonction main() sur la ligne située après la variable smartTvDevice :
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
}
  1. Sur la ligne qui suit la méthode turnOn(), appelez la méthode turnOff() :
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. Exécutez le code.

La sortie est la suivante :

Smart device is turned on.
Smart device is turned off.

5. Définir les propriétés de classe

Dans le module 1, vous avez appris ce qu'était une variable, à savoir un conteneur destiné à des éléments de données individuels. Vous avez également appris à créer une variable en lecture seule avec le mot clé val et une variable modifiable avec le mot clé var.

Alors que les méthodes définissent les actions qu'une classe peut effectuer, les propriétés définissent ses caractéristiques ou attributs de données. Par exemple, un appareil connecté possède les propriétés suivantes :

  • Nom. Nom de l'appareil.
  • Catégorie. Type d'appareil connecté (par exemple, divertissement, ou objet connecté pour la maison ou la cuisine).
  • État de l'appareil. Indique si l'appareil est allumé, éteint, en ligne ou hors connexion. L'appareil est considéré comme étant en ligne lorsqu'il est connecté à Internet. Sinon, il est considéré comme étant hors connexion.

Les propriétés sont essentiellement des variables définies dans le corps de la classe et non dans celui de la fonction. Cela signifie que la syntaxe utilisée pour définir les propriétés et les variables est identique. Vous définissez une propriété non modifiable avec le mot clé val et une propriété modifiable avec le mot clé var.

Implémentez les caractéristiques mentionnées ci-dessus en tant que propriétés de la classe SmartDevice :

  1. Sur la ligne située avant la méthode turnOn(), définissez la propriété name, puis affectez-la à une chaîne "Android TV" :
class SmartDevice {

    val name = "Android TV"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}
  1. Sur la ligne située après la propriété name, définissez la propriété category et affectez-la à une chaîne "Entertainment", puis définissez une propriété deviceStatus et affectez-la à une chaîne "online" :
class SmartDevice {

    val name = "Android TV"
    val category = "Entertainment"
    var deviceStatus = "online"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}
  1. Sur la ligne située après la variable smartTvDevice, appelez la fonction println(), puis transmettez-lui une chaîne "Device name is: ${smartTvDevice.name}" :
fun main() {
    val smartTvDevice = SmartDevice()
    println("Device name is: ${smartTvDevice.name}")
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. Exécutez le code.

La sortie est la suivante :

Device name is: Android TV
Smart device is turned on.
Smart device is turned off.

Fonctions getter et setter dans les propriétés

Les propriétés offrent davantage de possibilités que les variables. Imaginons que vous créiez une structure de classe représentant une smart TV. Une des actions que vous effectuez fréquemment consiste à augmenter et baisser le volume. Pour représenter cette action en programmation, vous pouvez créer une propriété nommée speakerVolume qui contient le niveau de volume actuel réglé sur l'enceinte du téléviseur. Cependant, il existe une plage de réglage du volume comprise entre 0 et 100. Pour vous assurer que la propriété speakerVolume ne dépasse jamais 100 ou n'est jamais inférieure à 0, vous pouvez écrire une fonction setter. Lorsque vous modifiez la valeur de la propriété, vous devez vérifier si elle est bien comprise entre 0 et 100. Prenons un autre exemple : supposons que le nom doive toujours être en majuscules. Vous pouvez implémenter une fonction getter pour convertir la propriété name en majuscules.

Avant d'aller plus loin dans l'implémentation de ces propriétés, vous devez comprendre toute la syntaxe utilisée pour les déclarer. La syntaxe complète permettant de définir une propriété modifiable commence par la définition de la variable, suivie des fonctions facultatives get() et set(). La syntaxe est illustrée dans le schéma ci-dessous :

f2cf50a63485599f.png

Lorsque vous ne définissez pas les fonctions getter et setter d'une propriété, le compilateur Kotlin les crée en interne. Par exemple, si vous utilisez le mot clé var pour définir une propriété speakerVolume et lui affecter une valeur 2, le compilateur génère automatiquement les fonctions getter et setter, comme vous pouvez le voir dans l'extrait de code ci-dessous :

var speakerVolume = 2
    get() = field  
    set(value) {
        field = value    
    }

Vous ne verrez pas ces lignes dans votre code, car le compilateur les ajoute en arrière-plan.

La syntaxe complète d'une propriété non modifiable présente deux différences :

  • Elle commence par le mot clé val.
  • Les variables de type val sont en lecture seule. Elles ne comportent donc pas de fonctions set().

Les propriétés Kotlin utilisent un champ de stockage pour conserver une valeur en mémoire. Un champ de stockage est essentiellement une variable de classe définie en interne dans les propriétés. Un champ de stockage est limité à une propriété, ce qui signifie que vous ne pouvez y accéder qu'à l'aide des fonctions de propriété get() ou set().

Pour lire la valeur de la propriété dans la fonction get() ou mettre à jour la valeur dans la fonction set(), vous devez utiliser le champ de stockage de la propriété. Il est généré automatiquement par le compilateur Kotlin et référencé avec un identifiant field.

Par exemple, si vous souhaitez mettre à jour la valeur de la propriété dans la fonction set(), vous devez utiliser le paramètre de la fonction set() (appelé paramètre value) et l'affecter à la variable field, comme vous pouvez le voir dans l'extrait de code ci-dessous :

var speakerVolume = 2
    set(value) {
        field = value    
    }

Par exemple, pour vous assurer que la valeur affectée à la propriété speakerVolume est comprise entre 0 et 100, vous pouvez implémenter la fonction setter, comme illustré dans l'extrait de code ci-dessous :

var speakerVolume = 2
    set(value) {
        if (value in 0..100) {
            field = value
        }
    }

Les fonctions set() vérifient si la valeur Int est comprise entre 0 et 100 en utilisant le mot clé in, suivi de la plage de valeurs. Si la valeur se trouve dans la plage attendue, la valeur field est mise à jour. Sinon, la valeur de la propriété ne change pas.

Vous allez inclure cette propriété dans une classe dans la section Implémenter une relation entre les classes de cet atelier. Il n'est donc pas nécessaire d'ajouter la fonction setter au code maintenant.

6. Définir un constructeur

L'objectif principal du constructeur est de spécifier comment les objets de la classe sont créés. En d'autres termes, les constructeurs initialisent un objet et font en sorte qu'il soit prêt à l'emploi. C'est ce que vous avez fait lors de l'instanciation de l'objet. Le code à l'intérieur du constructeur s'exécute lorsque l'objet de la classe est instancié. Vous pouvez définir un constructeur avec ou sans paramètres.

Constructeur par défaut

Un constructeur par défaut est un constructeur sans paramètres. Vous pouvez définir un constructeur de ce type comme indiqué dans cet extrait de code :

class SmartDevice constructor() {
    ...
}

La concision est l'une des caractéristiques du langage Kotlin. Vous pouvez donc supprimer le mot clé constructor si le constructeur ne contient ni annotations ni modificateurs de visibilité (nous reviendrons sur ce point dans une autre section). Vous pouvez également supprimer les parenthèses si le constructeur ne comporte aucun paramètre, comme illustré dans l'extrait de code ci-dessous :

class SmartDevice {
    ...
}

Le compilateur Kotlin génère automatiquement le constructeur par défaut. Ce constructeur n'est pas visible dans votre code, car le compilateur l'ajoute en arrière-plan.

Définir un constructeur paramétré

Dans la classe SmartDevice, les propriétés name et category ne sont pas modifiables. Vous devez vous assurer que toutes les instances de la classe SmartDevice initialisent les propriétés name et category. Dans l'implémentation actuelle, les valeurs des propriétés name et category sont codées en dur. Cela signifie que tous les appareils connectés sont nommés avec la chaîne "Android TV" et classés avec la chaîne "Entertainment".

Pour maintenir l'immuabilité tout en évitant les valeurs codées en dur, utilisez un constructeur paramétré pour les initialiser :

  • Dans la classe SmartDevice, déplacez les propriétés name et category vers le constructeur sans affecter de valeurs par défaut :
class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}

Le constructeur accepte à présent des paramètres pour configurer ses propriétés. Par conséquent, la façon d'instancier un objet pour une classe de ce type change également. Le schéma ci-dessous illustre la syntaxe complète pour instancier un objet :

bbe674861ec370b6.png

Voici la représentation du code :

SmartDevice("Android TV", "Entertainment")

Les deux arguments du constructeur sont des chaînes. Il n'est pas facile de déterminer à quel paramètre la valeur doit être affectée. Pour résoudre ce problème, de la même manière que vous avez transmis des arguments de fonction, vous pouvez créer un constructeur avec des arguments nommés, comme illustré dans cet extrait de code :

SmartDevice(name = "Android TV", category = "Entertainment")

En langage Kotlin, il existe deux principaux types de constructeurs :

  • Constructeur principal. Une classe ne peut avoir qu'un seul constructeur principal, qui est défini dans l'en-tête de la classe. Un constructeur principal peut être un constructeur par défaut ou paramétré. Le constructeur principal ne possède pas de corps, ce qui signifie qu'il ne peut pas contenir de code.
  • Constructeur secondaire. Une classe peut comporter plusieurs constructeurs secondaires. Vous pouvez définir le constructeur secondaire avec ou sans paramètres. Le constructeur secondaire peut initialiser la classe et a un corps qui peut contenir la logique d'initialisation. Si la classe comporte un constructeur principal, chaque constructeur secondaire doit l'initialiser.

Vous pouvez utiliser le constructeur principal pour initialiser les propriétés de l'en-tête de classe. Les arguments transmis au constructeur sont affectés aux propriétés. La syntaxe permettant de définir un constructeur principal commence par le nom de la classe, suivi du mot clé constructor et d'une paire de parenthèses. Les parenthèses contiennent les paramètres du constructeur principal. S'il y a plusieurs paramètres, des virgules sont utilisées pour séparer leurs définitions. Le schéma ci-dessous illustre la syntaxe complète permettant de définir un constructeur principal :

aa05214860533041.png

Le constructeur secondaire est inclus dans le corps de la classe et sa syntaxe se compose de trois parties :

  • Déclaration du constructeur secondaire. La définition du constructeur secondaire commence par le mot clé constructor, suivi de parenthèses. Le cas échéant, les parenthèses contiennent les paramètres requis par le constructeur secondaire.
  • Initialisation du constructeur principal. L'initialisation commence par le signe deux-points, suivi du mot clé this et d'une paire de parenthèses. Le cas échéant, les parenthèses contiennent les paramètres requis par le constructeur principal.
  • Corps du constructeur secondaire. L'initialisation du constructeur principal est suivie d'une paire d'accolades contenant le corps du constructeur secondaire.

La syntaxe est illustrée dans le schéma ci-dessous :

2dc13ef136009e98.png

Imaginons que vous souhaitiez intégrer une API développée par un fournisseur d'appareils connectés. Cependant, l'API renvoie un code d'état de type Int pour indiquer l'état initial de l'appareil. L'API renvoie une valeur 0 si l'appareil est hors connexion et une valeur 1 s'il est en ligne. Pour toute autre valeur entière, l'état est considéré comme inconnu. Vous pouvez créer un constructeur secondaire dans la classe SmartDevice afin de convertir le paramètre statusCode en une représentation sous forme de chaîne, comme vous pouvez le voir dans l'extrait de code suivant :

class SmartDevice(val name: String, val category: String) {
    var deviceStatus = "online"

    constructor(name: String, category: String, statusCode: Int) : this(name, category) {
        deviceStatus = when (statusCode) {
            0 -> "offline"
            1 -> "online"
            else -> "unknown"
        }
    }
    ...
}

7. Implémenter une relation entre les classes

Le concept d'héritage vous permet de créer une classe en vous appuyant sur les caractéristiques et le comportement d'une autre classe. Il s'agit d'un mécanisme efficace qui vous aide à écrire du code réutilisable et à établir des relations entre les classes.

On trouve aujourd'hui dans le commerce de nombreux appareils connectés, tels que des smart TV, des systèmes d'éclairage et des interrupteurs connectés. Lorsque vous représentez des appareils connectés en programmation, ils partagent certaines propriétés communes, telles que le nom, la catégorie et l'état. Ils ont également des comportements communs, comme la possibilité d'être allumés et éteints.

Cependant, la façon d'allumer ou d'éteindre chaque appareil connecté est différente. Par exemple, pour allumer un téléviseur, vous devrez peut-être allumer l'écran, puis régler la dernière chaîne et le dernier niveau de volume connus. En revanche, pour allumer une lumière, il vous suffira peut-être d'augmenter ou de diminuer la luminosité.

De plus, chaque appareil connecté permet d'effectuer d'autres fonctions et actions. Par exemple, sur un téléviseur, vous pouvez régler le volume et changer de chaîne. Dans le cas d'un système d'éclairage, vous pouvez régler la luminosité ou la couleur.

En bref, tous les appareils connectés présentent des fonctionnalités différentes, mais ils partagent aussi des caractéristiques communes. Vous pouvez soit dupliquer ces caractéristiques communes sur chacune des classes d'appareils connectés, soit rendre le code réutilisable avec le mécanisme d'héritage.

Pour ce faire, vous devez créer une classe parente SmartDevice, puis définir ces propriétés et comportements communs. Vous pouvez ensuite créer des classes enfants, telles que SmartTvDevice et SmartLightDevice, qui héritent des propriétés de la classe parente.

Dans le jargon de la programmation, on dit que les classes SmartTvDevice et SmartLightDevice étendent la classe parente SmartDevice. La classe parente est également appelée super-classe et la classe enfant, sous-classe. La relation entre ces classes est illustrée dans le schéma suivant :

Schéma représentant la relation d'héritage entre les classes.

Cependant, en langage Kotlin, toutes les classes sont dites "finales" par défaut, ce qui signifie que vous ne pouvez pas les étendre. Vous devez donc définir des relations entre elles.

Définissez la relation entre la super-classe SmartDevice et ses sous-classes :

  1. Dans la super-classe SmartDevice, ajoutez un mot clé open avant le mot clé class pour le rendre extensible :
open class SmartDevice(val name: String, val category: String) {
    ...
}

Le mot clé open informe le compilateur que cette classe est extensible, de sorte que d'autres classes puissent maintenant l'étendre.

La syntaxe utilisée pour créer une sous-classe commence par la création de l'en-tête de classe, comme vous l'avez fait jusqu'à présent. La parenthèse fermante du constructeur est suivie d'une espace, du signe deux-points, d'une autre espace, du nom de la super-classe et enfin d'une paire de parenthèses. Si nécessaire, les parenthèses contiennent les paramètres requis par le constructeur de super-classe. La syntaxe est illustrée dans le schéma ci-dessous :

1ac63b66e6b5c224.png

  1. Créez une sous-classe SmartTvDevice qui étend la super-classe SmartDevice :
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}

La définition constructor de la sous-classe SmartTvDevice ne précise pas si les propriétés sont modifiables ou non. Cela signifie que deviceName et deviceCategory sont de simples paramètres constructor plutôt que des propriétés de classe. Vous ne pourrez pas les utiliser dans la classe, mais vous les transmettrez simplement au constructeur de super-classe.

  1. Dans le corps de la sous-classe SmartTvDevice, ajoutez la propriété speakerVolume que vous avez créée lorsque vous avez étudié les fonctions getter et setter :
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
}
  1. Définissez une propriété channelNumber affectée à une valeur 1 avec une fonction setter qui spécifie une plage 0..200 :
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
}
  1. Définissez une méthode increaseSpeakerVolume() qui augmente le volume et imprime une chaîne "Speaker volume increased to $speakerVolume." :
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

     var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    } 
}
  1. Ajoutez une méthode nextChannel() qui augmente le numéro de chaîne et imprime une chaîne "Channel number increased to $channelNumber." :
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
    
    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }
}
  1. Sur la ligne située après la sous-classe SmartTvDevice, définissez une sous-classe SmartLightDevice qui étend la super-classe SmartDevice :
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}
  1. Dans le corps de la sous-classe SmartLightDevice, définissez une propriété brightnessLevel affectée à une valeur 0 avec une fonction setter qui spécifie une plage 0..100 :
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
}
  1. Définissez une méthode increaseBrightness() qui augmente la luminosité de l'éclairage et imprime une chaîne "Brightness increased to $brightnessLevel." :
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }
}

Relations entre les classes

Lorsque vous utilisez l'héritage, vous établissez une relation entre deux classes dans ce que l'on appelle une relation IS-A. Un objet est également une instance de la classe dont il hérite. Dans une relation HAS-A, un objet peut posséder une instance d'une autre classe sans être à proprement parler une instance de cette classe. Ces relations sont représentées dans le schéma ci-dessous :

Représentation des relations HAS-A et IS-A.

Relations IS-A

Lorsque vous spécifiez une relation IS-A (littéralement EST-UN ou EST-UNE) entre la super-classe SmartDevice et la sous-classe SmartTvDevice, cela signifie que la super-classe SmartDevice et la sous-classe SmartTvDevice ont des capacités identiques. La relation est unidirectionnelle. Vous pouvez donc dire que chaque smart TV est un appareil connecté, mais vous ne pouvez pas dire que chaque appareil connecté est une smart TV. La représentation du code d'une relation IS-A est illustrée dans l'extrait de code ci-dessous :

// Smart TV IS-A smart device.
class SmartTvDevice : SmartDevice() {
}

N'utilisez pas l'héritage dans le seul but de rendre le code réutilisable. Avant de prendre une décision, vérifiez si les deux classes sont liées. S'il existe une relation entre les deux, vérifiez si elles sont vraiment éligibles à la relation IS-A. Demandez-vous si une sous-classe est aussi une super-classe. Par exemple, Android est un système d'exploitation.

Relations HAS-A

HAS-A (littéralement A-UN ou A-UNE) est une autre méthode utilisée pour spécifier la relation entre deux classes. Par exemple, il est probable que vous utilisiez la smart TV chez vous. Dans ce cas, il existe une relation entre la smart TV et votre domicile. La maison contient un appareil connecté ou, en d'autres termes, elle a un appareil connecté. La relation HAS-A entre deux classes est également appelée composition.

Jusqu'à présent, vous avez créé quelques appareils connectés. Vous allez maintenant créer la classe SmartHome qui contient des appareils connectés. La classe SmartHome vous permet d'interagir avec les appareils connectés.

Utilisez une relation HAS-A pour définir une classe SmartHome :

  1. Définissez une classe SmartHome entre la classe SmartLightDevice et la fonction main() :
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

}

class SmartHome {
}

fun main() { 
    ...
}
  1. Dans le constructeur de classe SmartHome, utilisez le mot clé val pour créer une propriété smartTvDevice de type SmartTvDevice :
// The SmartHome class HAS-A smart TV device.
class SmartHome(val smartTvDevice: SmartTvDevice) {

}
  1. Dans le corps de la classe SmartHome, définissez une méthode turnOnTv() qui appelle la méthode turnOn() sur la propriété smartTvDevice :
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }
}
  1. Sur la ligne située après la méthode turnOnTv(), définissez une méthode turnOffTv() qui appelle la méthode turnOff() sur la propriété smartTvDevice :
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

}
  1. Sur la ligne située après la méthode turnOffTv(), définissez une méthode increaseTvVolume() qui appelle la méthode increaseSpeakerVolume() sur la propriété smartTvDevice, puis définissez une méthode changeTvChannelToNext() qui appelle la méthode nextChannel() sur la propriété smartTvDevice :
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }
}
  1. Dans le constructeur de la classe SmartHome, déplacez le paramètre de propriété smartTvDevice sur sa propre ligne, suivi d'une virgule :
class SmartHome(
    val smartTvDevice: SmartTvDevice,
) {

    ...

}
  1. Sur la ligne qui suit la propriété smartTvDevice, utilisez le mot clé val pour définir une propriété smartLightDevice de type SmartLightDevice :
// The SmartHome class HAS-A smart TV device and smart light.
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

}
  1. Dans la section SmartHome, définissez une méthode turnOnLight() qui appelle la méthode turnOn() sur l'objet smartLightDevice et une méthode turnOffLight() qui appelle la méthode turnOff() sur l'objet smartLightDevice :
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        smartLightDevice.turnOff()
    }
}
  1. Sur la ligne qui suit la méthode turnOffLight(), définissez une méthode increaseLightBrightness() qui appelle la méhtode increaseBrightness() sur la propriété smartLightDevice :
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        smartLightDevice.turnOff()
    }

    fun increaseLightBrightness() {
        smartLightDevice.increaseBrightness()
    }
}
  1. Sur la ligne qui suit la méthode increaseLightBrightness(), définissez une méthode turnOffAllDevices() qui appelle les mérthodes turnOffTv() et turnOffLight() :
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun turnOffAllDevices() {
        turnOffTv()
        turnOffLight()
    }
}

Remplacer les méthodes de super-classe à partir de sous-classes

Comme indiqué précédemment, même si la fonctionnalité d'activation et de désactivation est prise en charge par tous les appareils connectés, la façon dont ils l'exécutent est différente. Pour fournir ce comportement spécifique à un appareil, vous devez remplacer les méthodes turnOn() et turnOff() définies dans la super-classe. Dans ce contexte, "remplacer" signifie intercepter l'action, généralement pour en prendre le contrôle manuel. Lorsque vous remplacez une méthode, la méthode de la sous-classe interrompt l'exécution de celle définie dans la super-classe et fournit sa propre exécution.

Remplacez les méthodes turnOn() et turnOff() de la classe SmartDevice :

  1. Dans le corps de la super-classe SmartDevice, ajoutez un mot clé open avant le mot clé fun de chaque méthode :
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open fun turnOn() {
        // function body
    }

    open fun turnOff() {
        // function body
    }
}
  1. Dans le corps de la classe SmartLightDevice, définissez une méthode turnOn() avec un corps vide :
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
    }
}
  1. Dans le corps de la méthode turnOn(), définissez la propriété deviceStatus sur la chaîne "on", définissez la propriété brightnessLevel sur une valeur de 2, ajoutez une instruction println(), puis transmettez-lui une chaîne "$name turned on. The brightness level is $brightnessLevel." :
    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }
  1. Dans le corps de la classe SmartLightDevice, définissez une méthode turnOff() avec un corps vide :
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    fun turnOff() {
    }
}
  1. Dans le corps de la méthode turnOff(), définissez la propriété deviceStatus sur la chaîne "off", définissez la propriété brightnessLevel sur une valeur de 0, ajoutez une instruction println(), puis transmettez-lui une chaîne "Smart Light turned off" :
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    fun turnOff() {
        deviceStatus = "off"
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}
  1. Dans la sous-classe SmartLightDevice, avant le mot clé fun des méthodes turnOn() et turnOff(), ajoutez le mot clé override :
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        deviceStatus = "off"
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

Le mot clé override indique à l'environnement d'exécution Kotlin d'exécuter le code inclus dans la méthode définie dans la sous-classe.

  1. Dans le corps de la classe SmartTvDevice, définissez une méthode turnOn() avec un corps vide :
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
        
    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
        
    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }
    
    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    fun turnOn() {
    }
}
  1. Dans le corps de la méthode turnOn(), définissez la propriété deviceStatus sur la chaîne "on", ajoutez une instruction println(), puis transmettez-lui une chaîne "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " + "set to $channelNumber." :
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        deviceStatus = "on"
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }
}
  1. Dans le corps de la classe SmartTvDevice, après la méthode turnOn(), définissez une méthode turnOff() avec un corps vide :
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        ...
    }

    fun turnOff() {
    }
}
  1. Dans le corps de la méthode turnOff(), définissez la propriété deviceStatus sur la chaîne "off", ajoutez une instruction println(), puis transmettez-lui une chaîne "$name turned off" :
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        ...
    }

    fun turnOff() {
        deviceStatus = "off"
        println("$name turned off")
    }
}
  1. Dans la classe SmartTvDevice, avant le mot clé fun des méthodes turnOn() et turnOff(), ajoutez le mot clé override :
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        deviceStatus = "on"
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        deviceStatus = "off"
        println("$name turned off")
    }
}
  1. Dans la fonction main(), utilisez le mot clé var pour définir une variable smartDevice de type SmartDevice qui instancie un objet SmartTvDevice qui accepte un argument "Android TV" et un argument "Entertainment" :
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
}
  1. Sur la ligne qui suit la variable smartDevice, appelez la méthode turnOn() sur l'objet smartDevice :
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()
}
  1. Exécutez le code.

La sortie est la suivante :

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
  1. Sur la ligne qui suit l'appel de la méthode turnOn(), réaffectez la variable smartDevice pour instancier une classe SmartLightDevice qui accepte un argument "Google Light" et un argument "Utility". Appelez ensuite la méthode turnOn() sur la référence d'objet smartDevice :
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()
    
    smartDevice = SmartLightDevice("Google Light", "Utility")
    smartDevice.turnOn()
}
  1. Exécutez le code.

La sortie est la suivante :

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
Google Light turned on. The brightness level is 2.

Voici un exemple de polymorphisme. Le code appelle la méthode turnOn() sur une variable de type SmartDevice et, en fonction de la valeur réelle de la variable, différentes implémentations de la méthode turnOn() peuvent être exécutées.

Réutiliser du code de super-classe dans des sous-classes avec le mot clé super

Si vous examinez attentivement les méthodes turnOn() et turnOff(), vous remarquerez une similitude dans la façon dont la variable deviceStatus est mise à jour lorsque les méthodes sont appelées dans les sous-classes SmartTvDevice et SmartLightDevice : le code est dupliqué. Vous pouvez réutiliser le code lors de la mise à jour de l'état dans la classe SmartDevice.

Pour appeler la méthode remplacée dans la super-classe à partir de la sous-classe, vous devez utiliser le mot clé super. Appeler une méthode à partir de la super-classe revient à l'appeler depuis l'extérieur de la classe. Au lieu d'utiliser un opérateur . entre l'objet et la méthode, vous devez utiliser le mot clé super, qui indique au compilateur Kotlin d'appeler la méthode sur la super-classe plutôt que sur la sous-classe.

La syntaxe utilisée pour appeler la méthode à partir de la super-classe commence par un mot clé super, suivi de l'opérateur ., du nom de la fonction et d'une paire de parenthèses. Le cas échéant, les arguments sont placés entre parenthèses. La syntaxe est illustrée dans le schéma ci-dessous :

18cc94fefe9851e0.png

Réutilisez le code de la super-classe SmartDevice :

  1. Supprimez les instructions println() des méthodes turnOn() et turnOff(), puis déplacez le code dupliqué des sous-classes SmartTvDevice et SmartLightDevice vers la super-classe SmartDevice :
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open fun turnOn() {
        deviceStatus = "on"
    }

    open fun turnOff() {
        deviceStatus = "off"
    }
}
  1. Utilisez le mot clé super pour appeler les méthodes à partir de la classe SmartDevice dans les sous-classes SmartTvDevice et SmartLightDevice :
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

     var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        super.turnOn()
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        super.turnOff()
        println("$name turned off")
    }
}
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        super.turnOn()
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        super.turnOff()
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

Remplacer les propriétés de super-classe à partir de sous-classes

Vous pouvez remplacer les propriétés en suivant la même procédure que celle utilisée pour les méthodes.

Remplacez la propriété deviceType :

  1. Dans la super-classe SmartDevice, sur la ligne qui suit la propriété deviceStatus, utilisez les mots clés open et val pour définir une propriété deviceType définie sur une chaîne "unknown" :
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open val deviceType = "unknown"
    ...
}
  1. Dans la classe SmartTvDevice, utilisez les mots clés override et val pour définir une propriété deviceType définie sur une chaîne "Smart TV" :
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    ...
}
  1. Dans la classe SmartLightDevice, utilisez les mots clés override et val pour définir une propriété deviceType définie sur une chaîne "Smart Light" :
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    ...

}

8. Modificateurs de visibilité

Les modificateurs de visibilité jouent un rôle important pour l'obtention de l'encapsulation :

  • Dans une classe, ils vous permettent de masquer vos propriétés et méthodes afin d'empêcher tout accès non autorisé depuis l'extérieur.
  • Dans un package, ils vous permettent de masquer les classes et interfaces afin d'empêcher tout accès non autorisé depuis l'extérieur.

Kotlin propose quatre modificateurs de visibilité :

  • public. Modificateur de visibilité par défaut. Rend la déclaration accessible depuis n'importe où. Les propriétés et méthodes que vous souhaitez rendre accessibles à l'extérieur de la classe sont marquées comme publiques.
  • private. Rend la déclaration accessible dans le même fichier de classe ou le même fichier source.

Il existe probablement des propriétés et des méthodes qui ne sont utilisées qu'à l'intérieur de la classe et que vous ne souhaitez pas forcément rendre accessibles à d'autres classes. Ces propriétés et méthodes peuvent être marquées à l'aide du modificateur de visibilité private pour empêcher qu'une autre classe n'y accède accidentellement.

  • protected. Rend la déclaration accessible dans des sous-classes. Les propriétés et méthodes que vous souhaitez rendre accessibles dans la classe qui les définit et dans les sous-classes sont marquées à l'aide du modificateur de visibilité protected.
  • internal. Rend la déclaration accessible dans le même module. Le modificateur "internal" est semblable au modificateur "private", à la différence que vous pouvez accéder aux propriétés et méthodes internes depuis l'extérieur de la classe, à condition que l'accès s'effectue dans le même module.

Lorsque vous définissez une classe, elle est visible publiquement et tout package qui l'importe peut y accéder. Cela signifie qu'elle est publique par défaut, sauf si vous spécifiez un modificateur de visibilité. De même, lorsque vous définissez ou déclarez des propriétés et des méthodes dans la classe, elles sont accessibles par défaut en dehors de la classe via l'objet de classe. Il est essentiel de définir une visibilité appropriée pour le code, principalement pour masquer les propriétés et les méthodes auxquelles les autres classes ne doivent pas accéder.

Voyons, par exemple, de quelle façon une voiture est rendue accessible à un conducteur. Les spécificités des pièces qui composent la voiture et son fonctionnement interne sont masqués par défaut. La voiture est conçue pour être utilisée de la façon la plus intuitive possible. Vous ne voudriez pas que votre voiture soit aussi difficile à utiliser qu'un avion de ligne, tout comme vous ne voudriez pas qu'un autre développeur ou que votre futur "vous" ayez des doutes quant aux propriétés et méthodes d'une classe qui sont censées être utilisées.

Les modificateurs de visibilité vous aident à présenter les parties pertinentes du code à d'autres classes de votre projet et permettent d'éviter l'utilisation accidentelle de l'implémentation, ce qui rend le code plus facile à comprendre et moins sujet aux bugs.

Le modificateur de visibilité doit être placé avant la syntaxe de déclaration, tout en déclarant la classe, la méthode ou les propriétés, comme illustré dans le schéma ci-dessous :

dcc4f6693bf719a9.png

Spécifier un modificateur de visibilité pour les propriétés

La syntaxe permettant de spécifier le modificateur de visibilité d'une propriété commence par le modificateur private, protected ou internal, suivi de la syntaxe qui définit une propriété. La syntaxe est illustrée dans le schéma ci-dessous :

47807a890d237744.png

L'extrait de code ci-dessous, par exemple, vous montre comment rendre la propriété deviceStatus privée :

open class SmartDevice(val name: String, val category: String) {

    ...

    private var deviceStatus = "online"

    ...
}

Vous pouvez également définir les modificateurs de visibilité sur des fonctions setter. Le modificateur est placé avant le mot clé set. La syntaxe est illustrée dans le schéma ci-dessous :

cea29a49b7b26786.png

Pour la classe SmartDevice, la valeur de la propriété deviceStatus doit être lisible en dehors de la classe via des objets de classe. Toutefois, seuls la classe et ses enfants doivent être en mesure de mettre à jour ou d'écrire la valeur. Pour implémenter cette exigence, vous devez utiliser le modificateur protected sur la fonction set() de la propriété deviceStatus.

Utilisez le modificateur protected sur la fonction set() de la propriété deviceStatus :

  1. Dans la propriété deviceStatus de la super-classe SmartDevice, ajoutez le modificateur protected à la fonction set() :
open class SmartDevice(val name: String, val category: String) {

    ...

    var deviceStatus = "online"
        protected set(value) {
           field = value
       }

    ...
}

Vous n'effectuez aucune action ni vérification dans la fonction set(). Il vous suffit d'affecter le paramètre value à la variable field. Comme nous l'avons vu précédemment, cette méthode est semblable à l'implémentation par défaut des setters de propriété. Vous pouvez omettre les parenthèses et le corps de la fonction set() dans ce cas :

open class SmartDevice(val name: String, val category: String) {

    ...

    var deviceStatus = "online"
        protected set

    ...
}
  1. Dans la classe SmartHome, définissez une propriété deviceTurnOnCount sur une valeur 0 avec une fonction setter privée :
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    ...
}
  1. Ajoutez la propriété deviceTurnOnCount suivie de l'opérateur arithmétique ++ aux méthodes turnOnTv() et turnOnLight(), puis ajoutez la propriété deviceTurnOnCount suivie de l'opérateur arithmétique -- à turnOffTv() et turnOffLight() :
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    fun turnOnTv() {
        deviceTurnOnCount++
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        deviceTurnOnCount--
        smartTvDevice.turnOff()
    }
    
    ...

    fun turnOnLight() {
        deviceTurnOnCount++
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        deviceTurnOnCount--
        smartLightDevice.turnOff()
    }

    ...

}

Modificateurs de visibilité pour les méthodes

La syntaxe permettant de spécifier un modificateur de visibilité pour une méthode commence par le modificateur private, protected ou internal, suivi de la syntaxe qui définit une méthode. La syntaxe est illustrée dans le schéma ci-dessous :

e0a60ddc26b841de.png

Vous pouvez consulter l'extrait de code suivant pour savoir comment spécifier un modificateur protected pour la méthode nextChannel() dans la classe SmartTvDevice :

class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    protected fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }      

    ...
}

Modificateurs de visibilité pour les constructeurs

La syntaxe permettant de spécifier un modificateur de visibilité pour un constructeur est semblable à la définition du constructeur principal, à quelques différences près :

  • Le modificateur est spécifié après le nom de la classe, mais avant le mot clé constructor.
  • Si vous devez spécifier le modificateur pour le constructeur principal, vous devez conserver le mot clé et les parenthèses constructor, même en l'absence de paramètres.

La syntaxe est illustrée dans le schéma ci-dessous :

6832575eba67f059.png

Vous pouvez consulter l'extrait de code suivant pour savoir comment ajouter un modificateur protected au constructeur SmartDevice :

open class SmartDevice protected constructor (val name: String, val category: String) {

    ...

}

Modificateurs de visibilité pour les classes

La syntaxe permettant de spécifier un modificateur de visibilité pour une classe commence par le modificateur private, protected ou internal, suivi de la syntaxe qui définit une classe. La syntaxe est illustrée dans le schéma ci-dessous :

3ab4aa1c94a24a69.png

Vous pouvez consulter l'extrait de code suivant pour savoir comment spécifier un modificateur internal pour la classe SmartDevice :

internal open class SmartDevice(val name: String, val category: String) {

    ...

}

Idéalement, vous devez vous efforcer d'assurer une visibilité stricte des propriétés et des méthodes. Pour cela, vous devez les déclarer le plus souvent possible avec le modificateur private. Si vous ne pouvez pas maintenir leur caractère privé, utilisez le modificateur protected. Si vous ne pouvez pas assurer leur protection, utilisez le modificateur internal. Si vous ne pouvez pas maintenir leur caractère interne, utilisez le modificateur public.

Spécifier les modificateurs de visibilité appropriés

Ce tableau vous aide à déterminer les modificateurs de visibilité appropriés en fonction de l'endroit où la propriété ou les méthodes d'une classe ou d'un constructeur doivent être accessibles :

Modificateur

Accessible dans la même classe

Accessible dans la sous-classe

Accessible dans le même module

Accessible en dehors du module

private

𝗫

𝗫

𝗫

protected

𝗫

𝗫

internal

𝗫

public

Dans la sous-classe SmartTvDevice, vous ne devez pas autoriser le contrôle des propriétés speakerVolume et channelNumber depuis l'extérieur de la classe. Ces propriétés doivent uniquement être contrôlées via les méthodes increaseSpeakerVolume() et nextChannel().

De même, dans la sous-classe SmartLightDevice, la propriété brightnessLevel ne doit être contrôlée que via la méthode increaseLightBrightness().

Ajoutez les modificateurs de visibilité appropriés aux sous-classes SmartTvDevice et SmartLightDevice :

  1. Dans la classe SmartTvDevice, ajoutez un modificateur de visibilité private aux propriétés speakerVolume et channelNumber :
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    private var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    private var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    ...
}
  1. Dans la classe SmartLightDevice, ajoutez un modificateur private à la propriété brightnessLevel :
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    private var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    ...
}

9. Définir des délégués de propriété

Dans la section précédente, nous avons vu que les propriétés en langage Kotlin utilisaient un champ de stockage pour stocker leurs valeurs en mémoire. Utilisez l'identifiant field pour le référencer.

Si vous examinez le code écrit jusqu'à présent, vous pouvez voir le code dupliqué qui est utilisé pour vérifier si les valeurs se trouvent dans la plage définie pour les propriétés speakerVolume, channelNumber et brightnessLevel dans les classes SmartTvDevice et SmartLightDevice. Vous pouvez réutiliser le code de contrôle de plage dans la fonction setter avec des délégués. Le délégué se charge de la gestion de la valeur et vous ne devez donc pas utiliser de champ ni les fonctions getter et setter pour la gérer.

La syntaxe permettant de créer des délégués de propriété commence par la déclaration d'une variable, suivie du mot clé by, puis de l'objet délégué qui gère les fonctions getter et setter de la propriété. La syntaxe est illustrée dans le schéma ci-dessous :

928547ad52768115.png

Avant d'implémenter la classe à laquelle vous pouvez déléguer l'implémentation, vous devez vous familiariser avec les interfaces. Une interface est un contrat auquel les classes qui l'implémentent doivent se conformer. Elle met l'accent sur ce qu'il faut faire plutôt que sur la façon de le faire. En bref, une interface vous aide à réaliser l'abstraction.

Par exemple, avant de construire une maison, vous exposez vos desiderata à l'architecte. Vous voulez une chambre parentale, une chambre d'enfant, une salle de séjour, une cuisine et deux salles de bain. En bref, vous spécifiez ce que vous souhaitez et l'architecte spécifie comment arriver à ce résultat. La syntaxe permettant de créer une interface est illustrée dans le schéma ci-dessous :

bfe3fd1cd8c45b2a.png

Nous avons vu précédemment comment étendre une classe et comment remplacer ses fonctionnalités. Avec les interfaces, la classe implémente l'interface. La classe fournit les détails d'implémentation nécessaires pour les méthodes et propriétés déclarées dans l'interface. Vous procéderez de la même façon avec l'interface ReadWriteProperty pour créer le délégué. Les interfaces seront traitées plus en détail dans le module suivant.

Pour créer la classe déléguée pour le type var, vous devez implémenter l'interface ReadWriteProperty. De même, vous devez implémenter l'interface ReadOnlyProperty pour le type val.

Créez le délégué pour le type var :

  1. Avant la fonction main(), créez une classe RangeRegulator qui implémente l'interface Int> ReadWriteProperty<Any?, :
class RangeRegulator() : ReadWriteProperty<Any?, Int> {

}

fun main() {
    ...
}

Ne vous souciez pas des chevrons ni de ce qu'ils contiennent. Ils représentent des types génériques. Nous en reparlerons dans le module suivant.

  1. Dans le constructeur principal de la classe RangeRegulator, ajoutez un paramètre initialValue, une propriété minValue privée et une propriété maxValue privée, tous de type Int :
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

}
  1. Dans le corps de la classe RangeRegulator, remplacez les méthodes getValue() et setValue() :
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

Ces méthodes agissent comme fonctions getter et setter des propriétés.

  1. Sur la ligne qui précède la classe SmartDevice, importez les interfaces ReadWriteProperty et KProperty :
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

open class SmartDevice(val name: String, val category: String) {
    ...
}

...

class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

...
  1. Dans la classe RangeRegulator, sur la ligne qui précède la méthode getValue(), définissez une propriété fieldData et initialisez-la avec le paramètre initialValue :
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

Cette propriété sert de champ de stockage pour la variable.

  1. Dans le corps de la méthode getValue(), renvoyez la propriété fieldData :
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}
  1. Dans le corps de la méthode setValue(), vérifiez si le paramètre value en cours d'affectation se trouve dans la plage minValue..maxValue avant de l'affecter à la propriété fieldData :
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        if (value in minValue..maxValue) {
            fieldData = value
        }
    }
}
  1. Dans la classe SmartTvDevice, utilisez la classe déléguée pour définir les propriétés speakerVolume et channelNumber :
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    private var speakerVolume by RangeRegulator(initialValue = 2, minValue = 0, maxValue = 100)

    private var channelNumber by RangeRegulator(initialValue = 1, minValue = 0, maxValue = 200)

    ...

}
  1. Dans la classe SmartLightDevice, utilisez la classe déléguée pour définir la propriété brightnessLevel :
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    private var brightnessLevel by RangeRegulator(initialValue = 0, minValue = 0, maxValue = 100)

    ...

}

10. Tester la solution

Le code de la solution est présenté dans l'extrait de code suivant :

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"
        protected set

    open val deviceType = "unknown"

    open fun turnOn() {
        deviceStatus = "on"
    }

    open fun turnOff() {
        deviceStatus = "off"
    }
}

class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    private var speakerVolume by RangeRegulator(initialValue = 2, minValue = 0, maxValue = 100)

    private var channelNumber by RangeRegulator(initialValue = 1, minValue = 0, maxValue = 200)

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        super.turnOn()
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        super.turnOff()
        println("$name turned off")
    }
}

class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    private var brightnessLevel by RangeRegulator(initialValue = 0, minValue = 0, maxValue = 100)

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        super.turnOn()
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        super.turnOff()
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    fun turnOnTv() {
        deviceTurnOnCount++
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        deviceTurnOnCount--
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        deviceTurnOnCount++
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        deviceTurnOnCount--
        smartLightDevice.turnOff()
    }

    fun increaseLightBrightness() {
        smartLightDevice.increaseBrightness()
    }

    fun turnOffAllDevices() {
        turnOffTv()
        turnOffLight()
    }
}

class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        if (value in minValue..maxValue) {
            fieldData = value
        }
    }
}

fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()

    smartDevice = SmartLightDevice("Google Light", "Utility")
    smartDevice.turnOn()
}

La sortie est la suivante :

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
Google Light turned on. The brightness level is 2.

11. Relevez ce défi !

  • Dans la classe SmartDevice, définissez une méthode printDeviceInfo() qui imprime une chaîne "Device name: $name, category: $category, type: $deviceType".
  • Dans la classe SmartTvDevice, définissez une méthode decreaseVolume() qui diminue le volume et une méthode previousChannel() qui accède à la chaîne précédente.
  • Dans la classe SmartLightDevice, définissez une méthode decreaseBrightness() qui diminue la luminosité.
  • Dans la classe SmartHome, assurez-vous que toutes les actions ne peuvent être effectuées que lorsque la propriété deviceStatus de chaque appareil est définie sur une chaîne "on". Assurez-vous également que la propriété deviceTurnOnCount est mise à jour correctement.

Une fois l'implémentation terminée :

  • Dans la classe SmartHome, définissez les méthodes decreaseTvVolume(), changeTvChannelToPrevious(), printSmartTvInfo(), printSmartLightInfo() et decreaseLightBrightness().
  • Appelez les méthodes appropriées à partir des classes SmartTvDevice et SmartLightDevice dans la classe SmartHome.
  • Dans la fonction main(), appelez les méthodes qui ont été ajoutées pour les tester.

12. Conclusion

Félicitations ! Vous avez appris à définir des classes et à instancier des objets. Vous avez également appris à créer des relations entre les classes et à créer des délégués de propriété.

Résumé

  • La programmation orientée objet repose sur quatre grands principes : l'encapsulation, l'abstraction, l'héritage et le polymorphisme.
  • Les classes sont définies avec le mot clé class, et contiennent des propriétés et des méthodes.
  • Les propriétés sont semblables aux variables, si ce n'est qu'elles peuvent avoir des getters et des setters personnalisés.
  • Un constructeur indique comment instancier les objets d'une classe.
  • Vous pouvez omettre le mot clé constructor lorsque vous définissez un constructeur principal.
  • L'héritage facilite la réutilisation du code.
  • La relation IS-A fait référence à l'héritage.
  • La relation HAS-A fait référence à la composition.
  • Les modificateurs de visibilité jouent un rôle important pour l'obtention de l'encapsulation.
  • Kotlin propose quatre modificateurs de visibilité : public, private, protected et internal.
  • Un délégué de propriété vous permet de réutiliser le code getter et setter dans plusieurs classes.

En savoir plus