Apprenez en lisant dans l'ordre

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 code à ouvrir pour ajouter l'aperçu des vidéos
Ajouter l'aperçudes vidéosOuvrirpreviewOfModifier lafonction qui a labranche imageLes images peuventchanger aussiAjouter l'aperçudes vidéosÉcrireclass VideoFileImageFileintacteLes images nechangent pas
La rangée du haut modifie previewOf, la fonction qui contient aussi la branche des images. La rangée du bas ajoute seulement VideoFile et ne touche jamais à ImageFile.

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.

Sur une page de paiement, tu vas afficher le montant à payer pour chaque moyen de paiement, frais compris. PaymentMethod est déjà déclaré.

① Définis une classe pour le paiement en supérette qui hérite de PaymentMethod.

② De la même façon, définis une classe pour le paiement à la livraison.

③ Crée une carte bancaire, un paiement en supérette et un paiement à la livraison, puis affiche le montant à payer pour 4980 yens.

④ Affiche le montant à payer pour 12800 yens avec le paiement à la livraison.

(Si le code s'exécute correctement, une explication s'affiche.)

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

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
Quel preview s'exécute à chaque tour
Corps du for...offile.preview()Tour 1logo.pngTour 2manual.pdfTour 3memo.txtpreview deImageFilepreview dePdfFilepreview deAttachmentlogo.png :afficher laminiaturemanual.pdf :afficher la page 1memo.txt :pas d'aperçu
L'appel dans la boucle tient en une ligne, mais un corps de méthode différent s'exécute à chaque tour. Le preview exécuté dépend de la classe qui a créé le file de ce tour.

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.

Sur une page de confirmation de commande, tu vas proposer, pour chaque montant de commande, le moyen de paiement le moins cher. PaymentMethod, BankTransfer, PayLater et orderTotals sont déjà déclarés.

① Définis une classe pour le paiement en plusieurs fois, qui ajoute 3 % du prix en frais.

② Mets les trois moyens de paiement dans un même tableau.

③ Pour chaque montant de commande, choisis le moyen de paiement dont le montant à payer est le plus bas.

④ Affiche le moyen choisi et son montant à payer.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

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
Choisir les tours qui appellent cropButton
file.cropButton()sur chaque élémentAu tour 2, filevaut memo.txtAttachment n'apas de cropButtonLève uneTypeErrorTesterinstanceofImageFilefalse au tourde memo.txtPasse à la suitesans cropButtonSeul logo.pnga un bouton
La rangée du haut échoue au tour de memo.txt, qui n'a pas de cropButton. La rangée du bas ne l'appelle qu'aux tours ImageFile.

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).

Pour une commande payable en points, tu vas ajouter le solde de points restant, uniquement sur la ligne du paiement en points. PaymentMethod, PointPayment, methods, cartTotal et formatYen sont déjà déclarés.

① Parcours les moyens de paiement dans l'ordre et affiche le montant à payer pour chacun.

② Sur la ligne du paiement en points uniquement, ajoute à la fin les points qui restent après le paiement.

③ Affiche le nombre de moyens de paiement autres que les points.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

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
  }
}
Comment la liste traite folderLink
instanceofAttachmentPas créé avecune classeinstanceofvaut falseDossier devis :affichageimpossibletypeofitem.previewA une fonctionpreviewtypeof vaut"function"Dossier devis :ouvrir le lien
Le même folderLink s'affiche différemment selon la façon dont tu le testes. typeof ignore la classe et vérifie seulement que preview est une fonction.

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'appelerCe qui est vérifiéQuand l'utiliser
Appeler preview() directementRienTous les éléments ont preview
Tester instanceofCréé par ImageFile ou un enfant ?Méthode propre à cette classe
Tester typeofpreview 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.

QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Tu ajoutes PdfFile à une liste parcourue par une boucle qui appelle file.preview(). Que dois-tu ajouter ?

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 ?