Découverte de Swift (9)

Je continue ma découverte de Swift avec un nouveau chapitre de The Swift Programming Language consacré à l'héritage.

Définition de l'héritage

L'héritage, c'est le fait qu'une classe hérite des propriétés, des méthodes et d'autres caractéristiques d'une autre classe. La classe qui hérite est appelée subclass, ou classe-fille dans la langue de Molière, celle qui transmet ses caractéristiques est la superclass, classe-mère.

L'héritage est propre aux classes, c'est ce qui les différencie des autres types complexes comme les structures ou les énumérations avec lesquelles elles ont beaucoup de points communs, comme nous l'avons vu dans les chapitres précédents.

Une subclass peut utiliser les propriétés, méthodes et subscripts de sa superclass, elle peut même implémenter ses propres versions de ces propriétés, méthodes et subscripts, qui viennent alors se substituer à celles de la classe-mère.

Base class (classe de base)

Une classe de base, ou base class dans la langue d'Elton John, est une classe qui n'hérite pas d'une autre classe. Contrairement à d'autres languages, les classes dans Swift n'héritent pas toutes d'une classe-mère universelle.

Créons une première classe de base à titre d'exemple :

class Vehicle {
    
    var currentSpeed = 0.0
    
    var description: String {
        return "traveling at \(currentSpeed) km/h"
    }
    
    func makeNoise() {
        // Do nothing - an unknown vehicle doesn't necessarily make a noice
    }
}

Rien de bien compliqué ici : – une propriété stockée currentSpeed de type Double – une propriété calculée description de type String, avec un getter qui renvoie un texte précisant la vitesse du véhicule – une fonction makeNoise qui ne fait rien pour le moment

Créons ensuite notre premier véhicule :

let someVehicle = Vehicle()
print("Vehicle : \(someVehicle.description)")
// Prints "Vehicle : traveling at 0.0 km/h"

Sans surprise, nous avons un véhicule simple mais pas très utile, comme la classe à laquelle il appartient :–)

Subclass (classe-fille)

Restons dans le concret pour définir notre première classe-fille :

class Bicycle: Vehicle {
    var hasBasket = false
}

Nous avons ainsi créé une nouvelle classe Bicycle qui hérite de la classe Vehicle. Nous lui avons ajouté une propriété de type Bool qui indiquer si le vélo possède un panier.

C'est pratique, je peux maintenant créer mon propre vélo et lui poser un joli panier tout neuf :

let bicycle = Bicycle()
bicycle.hasBasket = true

Encore plus pratique, ma classe Bicycle a hérité des propriétés de sa classe-mère Vehicle, je peux donc ajuster la vitesse de mon vélo et constater le résultat :

bicycle.currentSpeed = 15.0
print("Bicycle: \(bicycle.description)")
// Prints "Bicycle: traveling at 15.0 km/h"

Allons encore plus loin : et si mon vélo était en fait un tandem ?

class Tandem: Bicycle {
    var currentNumberOfPassengers = 0
}

let tandem = Tandem()
tandem.hasBasket = true
tandem.currentNumberOfPassengers = 2
tandem.currentSpeed = 22.0
print("Tandem: \(tandem.description)")
// Prints "Tandem: traveling at 22.0 km/h"

Je peux ainsi construire toute une généalogie de classes héritant les unes des autres, en complétant et complexifiant à chaque niveau les caractéristiques propres à chaque classe pour refléter ses fonctionnalités.

Overriding

L'overriding, c'est le fait de redéfinir l'implémentation d'une propriété ou d'une méthode dans une classe-fille, pour que cette implémentation se substitue à celle normalement hérité de la classe-mère.

Pour cela, il faut préfixer la définition de la propriété ou méthode par le mot-clé override : cela permet d'éviter d'écraser involontairement une propriété ou méthode héritée d'une classe-mère. Le compilateur vérifie également que la propriété ou méthode substituée présente bien la même définition que celle dont elle hérite (même format, mêmes paramètres, etc.) même si l'implémentation est évidemment différente.

Accès à l'implémentation de la classe-mère

Même lorsqu'on décide de réimplémenter un élément de la classe-mère, on peut souhaiter faire appel à l'implémentation héritée. Par exemple, pour réaliser les opérations prévues par la méthode de la classe-mère, avant d'ajouter d'autres opérations spécifiques à la classe-fille.

Pour cela, le mot-clé super doit être utilisé. Par exemple : – dans l'implémentation d'une méthode override someMethod(), il est possible d'appeler la méthode de la classe-mère par super.someMethod() - même principe pour une propriétéoverride someProperty, avecsuper.somePropertypour accéder à la propriété de la classe-mère et notamment à ses implémentations degetetset. - pour les subscripts, le fonctionnement est similaire : on peut utiliser la syntaxesuper[index]` pour faire appel à l'implémentation de la classe-mère

Overriding d'une méthode

Revenons à notre exemple avec des véhicules pour montrer comment fonctionne l'overriding d'une méthode.

Dans la classe Vehicle, nous avions déclaré une méthode makeNoise() qui ne faisait pas grand chose. Nous pourrions la réimplémenter dans une classe-fille, pour qu'un train fasse un bruit caractéristique :

class Train: Vehicle {
    override func makeNoise() {
         print("Tchou tchou")
    }
}

var train = Train()
train.makeNoise()
// Prints "Tchou tchou"

Overriding d'une propriété

Surcharger une propriété dans une classe-fille permet : – soit d'implémenter ou réimplémenter un getter ou un setter spécifique – soit d'ajouter un observer sur une propriété de la classe-mère

Overriding des getters et setters

Il est possible d'implémenter dans une classe-fille un get et/ou un set pour une propriété héritée d'une classe-mère, que cette propriété soit stockée ou calculée.

Essayons de mettre cela en pratique en créant un nouveau type de véhicule :

class Car: Vehicle {
    var gear = 1
    
    override var description: String {
        return super.description + " in gear \(gear)"
    }
}

La classe Car hérite de la classe-mère Vehicle. Dans cette classe-fille, nous ajoutons : – une nouvelle propriété stockée gear pour gérer la boîte de vitesse – une ré-implémentation de la propriété calculée description qui renvoie la description “standard” de la classe-mère, suivie d'une information sur la boîte de vitesse spécifique aux voitures

Nous avons donc une classe-fille qui réutilise les fonctionnalités existantes de sa classe-mère tout en les étendant pour ses propres besoins :

let car = Car()
car.currentSpeed = 25.0
car.gear = 3
print("Car : \(car.description)")
// Prints "Car : traveling at 25.0 km/h in gear 3"

Overriding des observers

L'overriding permet également d'ajouter un observer sur une propriété stockée ou calculée héritée d'une classe-mère. Cela permet à la classe-fille d'être notifiée quand la valeur d'une propriété héritée change, quelle que soit la façon dont cette propriété est implémentée dans la classe-mère.

Prenons l'exemple d'une voiture automatique, que nous allons modéliser dans une classe-fille de la classe Car créée dans l'exemple précédent :

class AutomaticCar: Car {
    override var currentSpeed: Double {
        didSet {
            gear = Int(currentSpeed / 10.0 ) + 1
        }
    }
}

Cette classe AutomaticCar ne contente d'ajouter un observer didSet sur la propriété currentSpeed héritée de Car (ou plus précisémment, héritée de Car qui l'a elle-même hérité de Vehicle) : quand la vitesse de la voiture automatique, la boite de vitesse passe automatiquement sur le rapport adéquat (ne faisons pas plus attention que cela à la formule utilisée, ce n'est qu'un exemple).

En pratique, cela donne ceci :

let automaticCar = AutomaticCar()
automaticCar.currentSpeed = 35.0
print("Automatic Car : \(automaticCar.description)")
// Prints : "Automatic Car : traveling at 35.0 km/h in gear 4"

Contrairement à la voiture à boite manuelle de notre exemple précédent, nous n'avons pas besoin de régler ici à la fois la vitesse et le rapport de la boite de vitesse. Régler currentSpeed suffit à régler automatiquement gear : en roulant à 35 km/h, notre voiture automatique passe automatiquement en 4ème. Pratique, non ?

Empêcher l'overriding

Dans certains cas, on peut souhaiter que l'implémentation d'une propriété, d'une méthode ou d'un subscript ne puisse pas être modifiée dans une classe-fille. Dans ce cas, il faut utiliser le préfixe final dans la déclaration de l'élément concerné dans la classe-mère, par exemple final var, final func, ou final subscript. Le compilateur renverra alors une erreur si une classe-fille tente de réimplémenter l'élément concerné.

Il est même possible d'empêcher qu'une classe-fille soit créée pour une classe en particulier, en déclarant cette classe comme final class. Toute tentative d'hériter de cette classe sera refusée par le compilateur.

Et la suite ?

Le chapitre suivant du livre, qui s'intéresse aux initialisateurs, s'annonce costaud. En fonction de la densité du contenu et des exemples, je verrai si j'en tire un seul billet ou plusieurs si un découpage logique peut être fait.

#swift

Zéro Tech, le blog Tech de Zéro Janvier@zerojanvier@diaspodon.fr