Question 1Tu ajoutes PdfFile à une liste parcourue par une boucle qui appelle file.preview(). Que dois-tu ajouter ?
Polymorphisme — même appel, comportements différents
Une méthode du même nom dans chaque classe, et un seul appel pour toutes : tableaux mélangés, tests avec instanceof et duck typing.
Imagine un formulaire de contact qui liste ses pièces jointes, avec une miniature pour les images et un bouton de lecture pour les vidéos. Si tu stockes le type sous forme de chaîne et que tu fais des if dans la fonction d'affichage, tu dois rouvrir cette fonction et la compléter à chaque nouveau type.
Cet article traite du polymorphisme, où une méthode du même nom exécute un code différent selon la classe, et du duck typing, qui ne se soucie pas du tout de l'héritage.
Mettre les différences d'affichage dans les classes — des méthodes de même nom
Suppose que chaque pièce jointe soit un objet comme { kind: "video", fileName: "demo.mp4" }, et qu'une fonction previewOf compare kind. Pour ajouter les vidéos, tu ouvres previewOf, qui contient aussi la branche des images, et tu la complètes.
Avec le polymorphisme (donner à des classes différentes une méthode du même nom : l'appelant écrit toujours le même appel, et c'est le code de la classe qui a créé l'instance qui s'exécute), tu places l'affichage de chaque type dans le preview de la classe enfant correspondante.
Une fois que chaque classe enfant redéfinit le preview du parent, l'appelant écrit simplement preview() sans vérifier de quelle classe enfant il s'agit.
// Stocker le type sous forme de chaîne et faire des if dans la fonction d'affichage
function previewOf(file) {
if (file.kind === "image") return `${file.fileName} : afficher la miniature`;
return `${file.fileName} : pas d'aperçu`;
}
console.log(previewOf({ kind: "video", fileName: "demo.mp4" })); // demo.mp4 : pas d'aperçu
// Écrire une méthode preview de même nom dans la classe de chaque type
class Attachment {
constructor(fileName) { this.fileName = fileName; }
preview() { return `${this.fileName} : pas d'aperçu`; }
}
class ImageFile extends Attachment {
preview() { return `${this.fileName} : afficher la miniature`; }
}
class VideoFile extends Attachment {
preview() { return `${this.fileName} : afficher le bouton lecture`; }
}
console.log(new ImageFile("logo.png").preview()); // logo.png : afficher la miniature
console.log(new VideoFile("demo.mp4").preview()); // demo.mp4 : afficher le bouton lecture
Le premier console.log a affiché « pas d'aperçu » parce que previewOf n'a pas encore de branche "video". Avec les classes, la personne qui ajoute les vidéos n'a qu'à reprendre le nom preview, sans avoir à retenir quelles chaînes vont dans kind.
Appeler d'un coup des types mélangés — tableaux et for...of
La liste arrive sous la forme d'un seul tableau qui mélange des images, des PDF et des fichiers de type inconnu. Si tu mets chaque élément dans sa propre variable et que tu écris un appel à preview() pour chacun, tu devras ajouter ou retirer des appels chaque fois que le nombre de pièces jointes change.
Tu peux mettre des instances de classes différentes dans un même tableau. Quand tu le parcours avec for...of, le file.preview() du corps de la boucle s'exécute à chaque tour sur une instance d'une classe différente, et l'appel tient toujours en une ligne.
Quand tu ajoutes PdfFile pour les PDF, tu ne touches pas à la boucle.
// Attachment et ImageFile sont définies comme dans le premier exemple
// Un type ajouté après coup. La boucle ne change pas
class PdfFile extends Attachment {
preview() { return `${this.fileName} : afficher la page 1`; }
}
const attachments = [
new ImageFile("logo.png"),
new PdfFile("manual.pdf"),
new Attachment("memo.txt"),
];
// Le corps tient en une ligne. À chaque tour, file contient une instance d'une classe différente
for (const file of attachments) {
console.log(file.preview());
}
// logo.png : afficher la miniature
// manual.pdf : afficher la page 1
// memo.txt : pas d'aperçu
Au troisième tour, memo.txt a été créé à partir de Attachment : c'est donc le preview d'Attachment lui-même qui s'exécute. Si une nouvelle classe enfant oublie de définir preview, elle retombe sur ce même affichage au lieu de lever une erreur : chaque fois que tu ajoutes un type, vérifie sa ligne dans la liste.
Appeler du code propre à un seul type — tester avec instanceof
Il faut maintenant ajouter un bouton de recadrage aux lignes d'images de la liste. cropButton, qui renvoie ce bouton, n'est définie que dans ImageFile : appeler file.cropButton() sur chaque élément de la boucle lève donc une TypeError au tour de memo.txt.
instanceof renvoie true si la valeur de gauche a été créée à partir de la classe de droite ou de l'une de ses classes enfants. Teste file instanceof ImageFile et n'appelle cropButton qu'aux tours où c'est vrai.
Comme toutes les classes ont preview, tu peux l'appeler directement, sans test.
class Attachment {
constructor(fileName) { this.fileName = fileName; }
preview() { return `${this.fileName} : pas d'aperçu`; }
}
class ImageFile extends Attachment {
preview() { return `${this.fileName} : afficher la miniature`; }
cropButton() { return `${this.fileName} : afficher le bouton recadrer`; } // Seule ImageFile l'a
}
const attachments = [new ImageFile("logo.png"), new Attachment("memo.txt")];
for (const file of attachments) {
console.log(file.preview()); // Toutes les classes l'ont : on l'appelle sans test
if (file instanceof ImageFile) {
console.log(file.cropButton()); // Appelée seulement aux tours ImageFile
}
}
// logo.png : afficher la miniature
// logo.png : afficher le bouton recadrer
// memo.txt : pas d'aperçu
La rangée du haut échoue avec TypeError: file.cropButton is not a function. Si tu testes instanceof même pour preview, tu finis par ajouter un test par type, exactement comme avec previewOf. Réserve les tests instanceof aux méthodes que seules certaines classes possèdent.
Avec !, mets instanceof entre parenthèses
Dans !file instanceof ImageFile, !file est évalué en premier et donne false : le test devient en réalité false instanceof ImageFile, qui vaut toujours false. Pour dire « n'est pas une ImageFile », mets le test entre parenthèses : !(file instanceof ImageFile).
Lister des valeurs qui n'héritent pas — le duck typing
La liste doit aussi afficher des liens vers des dossiers. Un lien est un littéral objet doté d'une méthode preview(), et il n'hérite pas de Attachment. Si tu testes instanceof Attachment pour écarter les anciennes données sans preview, comme { fileName: "old.doc" }, le lien donne lui aussi false et finit, comme les anciennes données, en « affichage impossible ».
Avec le duck typing (décider comment traiter une valeur uniquement selon qu'elle possède les méthodes dont tu as besoin, sans vérifier sa classe ni son héritage), tu testes typeof item.preview === "function".
Un appel de méthode fonctionne quelle que soit la classe, du moment que la valeur à gauche du point possède une fonction de ce nom.
class Attachment {
constructor(fileName) { this.fileName = fileName; }
preview() { return `${this.fileName} : pas d'aperçu`; }
}
// Un lien créé sans classe. Il n'hérite pas de Attachment
const folderLink = {
fileName: "Dossier devis",
preview() { return `${this.fileName} : ouvrir le lien`; },
};
console.log(folderLink instanceof Attachment); // false
// On décide selon que preview est une fonction, pas selon la classe
for (const item of [new Attachment("memo.txt"), folderLink, { fileName: "old.doc" }]) {
if (typeof item.preview === "function") {
console.log(item.preview()); // memo.txt : pas d'aperçu / Dossier devis : ouvrir le lien
} else {
console.log(`${item.fileName} : affichage impossible`); // old.doc : affichage impossible
}
}
Comme il n'y a pas de test instanceof, une valeur dont la fonction preview ne renvoie pas de chaîne passe aussi. Veille à ce que chaque valeur de la liste ait un preview() qui renvoie une chaîne à afficher. Le tableau ci-dessous compare les trois façons d'appeler vues dans cet article.
| Façon d'appeler | Ce qui est vérifié | Quand l'utiliser |
|---|---|---|
| Appeler preview() directement | Rien | Tous les éléments ont preview |
| Tester instanceof | Créé par ImageFile ou un enfant ? | Méthode propre à cette classe |
| Tester typeof | preview est une fonction ? | Accepter toute valeur avec preview |
in renvoie true même si preview n'est pas une fonction
"preview" in item vaut true même quand la valeur est une chaîne, comme dans { preview: "Bientôt disponible" }. Appeler item.preview() sur cette valeur lève TypeError: item.preview is not a function : avant de l'appeler, vérifie avec typeof qu'il s'agit bien d'une fonction.
Vérification des connaissances
Répondez à chaque question une par une.
Question 2Pour memo, créé à partir de Attachment, que vaut !memo instanceof ImageFile ?
Question 3Pour un littéral objet link doté d'une méthode preview(), que valent link instanceof Attachment et typeof link.preview ?