IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

JavaScript Éloquent

Une introduction moderne à la programmation
Image non disponible


précédentsommairesuivant

VIII. Programmation orientée objet

Au début des années 90, une chose appelée programmation orientée objet souffla un vent nouveau sur l'industrie du logiciel. La plupart des idées derrière ce concept n'étaient pas vraiment nouvelles, mais elles avaient enfin suffisamment d'élan pour décoller, et devenir « à la mode ». Des livres furent écrits sur le sujet, des cours furent organisés, des langages de programmation développés. Tout d'un coup, tout le monde se mit à vanter les mérites de la programmation orientée objet, appliquant ses recettes à tous les problèmes avec enthousiasme, se convainquant qu'on avait enfin trouvé la bonne façon d'écrire des programmes.

Ces choses arrivent souvent. Quand un problème est compliqué. Les gens cherchent toujours une solution magique. Quand arrive quelque chose qui ressemble à cette solution, ils sont prêts à s'y jeter corps et âme. Pour de nombreux programmeurs, encore aujourd'hui, l'orientation objet (ou du moins la vision qu'ils en ont) est la panacée. Si un programme n'est pas « en pur objet », quel que soit le sens de cette expression, il est considéré comme résolument inférieur.

Toutefois, peu d'engouements ont duré si longtemps. La longévité de la programmation orientée objet peut sûrement s'expliquer par le fait que les idées centrales du concept sont utiles et simples. Dans ce chapitre, nous allons parler de ces idées et de leur application, plutôt excentrique, au JavaScript. Les paragraphes précédents n'étaient absolument pas destinés à discréditer ces idées. Mon objectif était juste d'éviter qu'on ne jure plus que par elles.

Comme son nom l'indique, la programmation orientée objet est centrée sur la notion d'objet. Depuis le début, nous avons utilisé les objets comme des espèces de fourre-tout plein de valeurs, où l'on ajoute ou modifie des propriétés à notre guise. En fait, dans une approche orientée objet, les objets sont vus comme des microcosmes indépendants, qui ne communiquent avec l'extérieur qu'à travers un nombre limité d'interfaces, un ensemble de méthodes et propriétés spécifiques. La « liste des nœuds atteints » utilisée à la fin du chapitre 7 en est un exemple : nous avons utilisé trois fonctions, creerListePointsParcourus, stockerPointsParcourus et trouverPointsParcourus pour interagir avec elle. Ces trois fonctions forment une interface pour cette sorte d'objets.

Les objets Date, Error et BinaryHeap que nous avons vus fonctionnent également comme cela. Au lieu de fournir des fonctions classiques pour travailler avec ces objets, ils fournissent une manière d'être créés, via le mot-clé new, et un certain nombre de méthodes et propriétés qui forment le reste de l'interface.

Pour faire une méthode d'objet, il suffit de définir une variable qui contiendra une fonction.

 
Sélectionnez
var lapin = {};
lapin.parler = function(tirade) {
  print("Le lapin dit '", tirade, "'");
};
 
lapin.parler("Eh bien, maintenant c'est vous qui me le demandez.");

Dans la plupart des cas, la méthode aura besoin de savoir sur qui elle doit s'appliquer. Par exemple, s'il y a plusieurs lapins, la méthode parler doit pouvoir indiquer quel est le lapin qui parle. Pour ce faire, il y a une variable spéciale appelée this, qui est toujours définie à l'intérieur d'une fonction et qui pointe vers l'objet sur lequel la fonction s'applique. Une fonction est appelée une méthode quand elle est définie en tant que propriété d'un objet, et appelée directement comme dans objet.methode().

 
Sélectionnez
function parler(tirade) {
  print("Le lapin ", this.adjectif, " dit « ", tirade, " »");
}
var lapinBlanc = {adjectif: "blanc", parler: parler};
var grosLapin = {adjectif: "gras", parler: parler};
 
lapinBlanc.parler("Par ma moustache et mes oreilles, comme il se fait tard !");
grosLapin.parler("J'ai bien envie d'une carotte, maintenant.");

Je peux maintenant clarifier la présence du mystérieux premier argument de la méthode apply, pour lequel nous avons toujours mis null dans le chapitre 6. Cet argument peut être utilisé pour spécifier un objet sur lequel la fonction s'appliquera, qui prendra donc le rôle de this. Toutefois, pour les fonctions qui ne sont pas des méthodes, cela n'a pas de sens, d'où le null.

 
Sélectionnez
parler.apply(grosLapin, ["Miam."]);

Les fonctions ont également une méthode call, qui se comporte comme apply, à l'exception du fait que les arguments peuvent être fournis séparément, et non dans un tableau :

 
Sélectionnez
parler.call(grosLapin, "Rot.");

Le mot-clé new fournit un bon moyen de créer de nouveaux objets. Quand une fonction est appelée avec le mot new devant, sa variable this pointe sur un nouvel objet, qui sera automatiquement retourné (à moins que la fonction ne retourne explicitement autre chose). Les fonctions utilisées pour créer de nouveaux objets comme ça sont appelées des constructeurs. En voici un pour les lapins :

 
Sélectionnez
function Lapin(adjectif) {
  this.adjectif = adjectif;
  this.parler = function(tirade) {
    print("Le lapin ", this.adjectif, " dit '", tirade, "'");
  };
}
 
var lapinTueur = new Lapin("tueur");
lapinTueur.parler("GRAAAAAAAAAH !");

Il y a une convention, parmi les programmeurs JavaScript, qui consiste à faire commencer les noms de constructeurs par une lettre majuscule. Cela permet de mieux les reconnaître au milieu des autres fonctions.

Mais pourquoi le mot-clé new est-il nécessaire ? Après tout, nous aurions pu écrire simplement :

 
Sélectionnez
function creerLapin(adjectif) {
  return {
    adjectif: adjectif,
    parler: function(tirade) {/*etc.*/}
  };
}
 
var lapinNoir = creerLapin("noir");

Mais ce n'est pas exactement la même chose. new en fait discrètement plus. En fait, notre fonction lapinTueur a une propriété appelée constructor, qui pointe vers la fonction Lapin l'ayant créée. lapinNoir a également cette propriété, mais elle pointe vers la fonction Object.

 
Sélectionnez
show(lapinTueur.constructor);
show(lapinNoir.constructor);

D'où vient la propriété constructor ? Elle fait partie du prototype d'un lapin. Les prototypes sont une partie importante du fonctionnement des objets en JavaScript. Chaque objet est basé sur un prototype, qui lui confère un ensemble de propriétés. Les objets simples que nous avons utilisés jusque-là sont tous basés sur le plus élémentaire des prototypes, celui associé au constructeur Object. En fait, taper {} est équivalent à taper new Object().

 
Sélectionnez
var objetSimple = {};
show(objetSimple.constructor);
show(objetSimple.toString);

toString est une méthode qui fait partie du prototype Object. Ça signifie que tous les objets de base ont une méthode toString, qui les convertit en chaîne de caractères. Nos objets lapin sont basés sur le prototype associé au constructeur Lapin. Il est possible d'utiliser la propriété prototype d'un constructeur pour accéder à... leur prototype :

 
Sélectionnez
show(Lapin.prototype);
show(Lapin.prototype.constructor);

Chaque fonction est automatiquement munie d'une propriété prototype, dont la propriété constructor renvoie à la fonction. Puisque le prototype « lapin » est lui-même un objet, il est basé sur le prototype Object, et partage sa méthode toString.

 
Sélectionnez
show(lapinTueur.toString == objetSimple.toString);

Même si les objets semblent partager des propriétés avec leur prototype, ce partage n'est qu'à sens unique. Les propriétés des prototypes influencent les objets basés dessus, mais les propriétés de cet objet ne changent jamais le prototype.

Les règles sont précisément les suivantes : pour trouver la valeur d'une propriété, JavaScript cherche d'abord parmi les propriétés de l'objet lui-même. S'il y a une propriété qui porte le nom que l'on recherche, c'est sa valeur que l'on obtient. S'il n'y en a pas, la recherche se poursuit à travers le prototype de l'objet, et ensuite à travers le prototype du prototype, et ainsi de suite. Si aucune propriété n'est trouvée, c'est la valeur undefined qui est renvoyée. À l'inverse, lorsqu'on définit la valeur d'une propriété, JavaScript ne remonte jamais au prototype, il attribue directement la valeur à une propriété de l'objet lui-même.

 
Sélectionnez
Lapin.prototype.dents = "petites";
show(lapinTueur.dents);
lapinTueur.dents = "longues, pointues et sanglantes";
show(lapinTueur.dents);
show(Lapin.prototype.dents);

Cela signifie que le prototype peut être utilisé pour ajouter des propriétés et des méthodes à tous les objets basés dessus. Par exemple, il se peut que nos lapins aient soudainement besoin de danser.

 
Sélectionnez
Lapin.prototype.danser = function() {
  print("Le lapin ", this.adjectif, " danse une gigue.");
};
 
lapinTueur.danser();

Et, comme vous vous en doutez, le prototype de lapin est le meilleur endroit où ajouter des éléments communs à tous les lapins, comme la méthode parler. Voici donc une nouvelle approche pour notre constructeur de Lapin :

 
Sélectionnez
function Lapin(adjectif) {
  this.adjectif = adjectif;
}
Lapin.prototype.parler = function(tirade) {
  print("Le lapin ", this.adjectif, " dit '", tirade, "'");
};
 
var noisetteLeLapin = new Lapin("noisette");
noisetteLeLapin.parler("Good Frith!");

Le fait que tous les objets aient leur prototype et reçoivent des propriétés de ce prototype peut apporter quelques complications. Ça signifie qu'utiliser un objet pour stocker des trucs, comme les chats du chapitre 4, peut mal se passer. Par exemple, si nous nous étions demandé s'il y a un chat nommé « constructor », nous aurions implémenté le test suivant :

 
Sélectionnez
var pasUnSeulChat = {};
if ("constructor" in pasUnSeulChat)
  print("Oui, il y a sans aucun doute un chat appelé « constructor ».");

C'est problématique. Un autre problème tient au fait qu'il est souvent pratique d'étendre les prototypes des constructeurs standards comme Object ou Array avec de nouvelles fonctions. Par exemple, nous pouvons donner à tous les objets une méthode nommée properties, qui retourne un tableau contenant le nom des propriétés (non cachées) d'un objet.

 
Sélectionnez
Object.prototype.properties = function() {
  var resultat = [];
  for (var property in this)
    resultat.push(property);
  return resultat;
};
 
var test = {x: 10, y: 3};
show(test.properties());

Et cela met tout de suite le problème en évidence. Maintenant que le prototype Object a une propriété appelée properties, parcourir les propriétés de n'importe quel objet, en utilisant for et in, renverra également cette propriété partagée, ce qui n'est généralement pas ce que nous souhaitons. Nous sommes seulement intéressés par les propriétés que l'objet a lui-même.

Heureusement, il y a un moyen de trouver si une propriété appartient à un objet lui-même, ou à l'un de ses prototypes. Malheureusement, cela complique un peu le parcours des propriétés d'un objet. Tout objet a une méthode appelée hasOwnProperty, qui nous indique si l'objet a une propriété de ce nom. En se basant sur ce mécanisme, nous pouvons réécrire notre méthode properties de la manière suivante :

 
Sélectionnez
Object.prototype.properties = function() {
  var resultat = [];
  for (var property in this) {
    if (this.hasOwnProperty(property))
      resultat.push(property);
  }
  return resultat;
};
 
var test = {"Gros Igor": true, "Boule de Feu": true};
show(test.properties());

Et bien sûr, nous pouvons abstraire cela dans une fonction de haut niveau. Notez que la fonction action est appelée avec à la fois le nom de la propriété et la valeur qu'elle a dans l'objet.

 
Sélectionnez
function forEachIn(objet, action) {
  for (var property in objet) {
    if (objet.hasOwnProperty(property))
      action(property, objet[property]);
  }
}
 
var chimere = {visage: "lion", corps: "chèvre", derrière: "serpent"};
forEachIn(chimere, function(nom, valeur) {
  print("Un ", nom, " de ", valeur, ".");
});

Mais, que se passe-t-il si on rencontre un chat nommé hasOwnProperty ? (On ne sait jamais.) Il sera stocké dans l'objet, et la tentative suivante de parcourir la collection de chats, utilisant objet.hasOwnProperty, sera un échec, car cette propriété ne pointera plus vers la fonction. Une façon d'éviter ce problème est d'agir encore plus salement :

 
Sélectionnez
function forEachIn(objet, action) {
   for (var property in objet) {
       if (Object.prototype.hasOwnProperty.call(objet, property))
           action(property, objet[property]);
   }
}
 
var test = {name: "Mardochée", hasOwnProperty: "Oh-oh"};
forEachIn(test, function (nom, valeur) {
   print ("Property ", nom, " = ", valeur);
});

(Note : cet exemple ne fonctionne pas pour l'instant correctement dans Internet Explorer 8, qui a semble-t-il, des problèmes avec la redéfinition des propriétés intégrées.)

Ici, au lieu d'utiliser la méthode trouvée dans l'objet lui-même, nous prenons la méthode fournie par le prototype Object, et l'appliquons en utilisant call sur le bon objet. À moins que quelqu'un n'ait joué avec la méthode de Object.prototype (et ne faites pas ça), le programme devrait fonctionner correctement.

hasOwnProperty peut également être utilisée dans les situations où l'on utilise l'opérateur in pour savoir si un objet contient une propriété particulière. Mais il y a encore une subtilité. Nous avons vu dans le chapitre 4 que certaines propriétés, comme toString, sont 'cachées', et ne sont donc pas considérées lors du parcours des éléments d'un objet via une instruction for/in. Il s'avère que les navigateurs de la famille Gecko (Firefox principalement) donnent à chaque objet une propriété cachée nommée __proto__, qui pointe vers le prototype de cet objet. hasOwnProperty retourne true, pour cette propriété, même si le programme ne l'a pas explicitement ajoutée. Avoir accès au prototype d'un objet peut être très pratique, mais en faire une propriété comme ça n'était pas une très bonne idée. Toutefois, Firefox est un navigateur Web très utilisé, et il convient de faire attention à cela quand vous écrivez des programmes pour le Web. Il y a une méthode propertyIsEnumerable, qui retourne false, pour les propriétés cachées, et peut donc être utilisée pour filtrer les étrangetés comme __proto__. Pour contourner ce problème de manière fiable, on peut utiliser une expression comme :

 
Sélectionnez
var objet = {foo: "bar"};
show (Object.prototype.hasOwnProperty.call(objet, "foo") &&
   Object.prototype.propertyIsEnumerable.call(objet, "foo"));

Simple et agréable n'est-ce pas ? C'est l'un des aspects de JavaScript qui ne sont pas-si-bien-conçus-que-ça. Les objets jouent à la fois le rôle de « valeurs avec méthodes », qui fonctionnent très bien avec les prototypes, et « d'ensembles de propriétés », pour lesquels les prototypes ne font que déranger.

Écrire l'expression ci-dessus à chaque fois qu'on a besoin de vérifier la présence d'une propriété dans un objet n'est pas viable. Nous pourrions le mettre dans une fonction, mais une meilleure approche est encore d'écrire un constructeur et un prototype dédié aux situations où nous voulons utiliser un objet simplement comme un ensemble de propriétés. Puisqu'il est prévu pour pouvoir y chercher des choses par leurs noms, nous l'appellerons Dictionary (dictionnaire).

 
Sélectionnez
function Dictionary(valeursInitiales) {
  this.valeurs = valeursInitiales || {};
}
Dictionary.prototype.store = function(nom, valeur) {
  this.valeurs[nom] = valeur;
};
Dictionary.prototype.lookup = function(nom) {
  return this.valeurs[nom];
};
Dictionary.prototype.contains = function(nom) {
  return Object.prototype.hasOwnProperty.call(this.valeurs, nom) &&
    Object.prototype.propertyIsEnumerable.call(this.valeurs, nom);
};
Dictionary.prototype.each = function(action) {
  forEachIn(this.valeurs, action);
};
 
var couleurs = new Dictionary({Grover: "bleu",
                              Elmo: "orange",
                              Bart: "jaune"});
show(couleurs.contains("Grover"));
show(couleurs.contains("constructor"));
couleurs.each(function(nom, couleur) {
  print(nom, " est ", couleur);
});

Désormais, tous les inconvénients de l'utilisation des objets en tant qu'ensemble de propriétés sont « cachés » derrière une interface pratique : un constructeur et quatre méthodes. Notez que la propriété valeurs d'un objet Dictionary ne fait pas partie de son interface, ce n'est qu'un détail interne, et quand vous utilisez des objets Dictionary, vous n'avez pas besoin de l'utiliser directement.

Chaque fois que vous écrivez une interface, il est utile d'y ajouter un commentaire retraçant rapidement ce qu'elle fait et comment l'utiliser. De cette manière, quand quelqu'un (éventuellement vous dans trois mois) souhaite travailler avec cette interface, il peut se faire rapidement une idée de comment l'utiliser, et n'a alors pas besoin d'étudier tout le programme.

La plupart du temps, quand vous concevez une nouvelle interface, vous êtes rapidement confronté à des problèmes et des limitations, quel que soit votre sujet, et vous changez en conséquence votre interface. Pour éviter de perdre du temps, il est conseillé de ne documenter vos interfaces qu'après les avoir expérimentées un certain temps sur des cas concrets, et qu'elles se soient révélées efficaces. Bien sûr, cela pourrait augmenter la tentation de ne pas documenter du tout. Mais personnellement, je considère la documentation comme une « touche finale », à ajouter au système. Quand ça donne l'impression d'être prêt, c'est qu'il est temps d'écrire un peu sur le sujet, et de voir si ça sonne aussi bien en français (ou n'importe quelle autre langue vivante), qu'en JavaScript (où n'importe quel autre langage de programmation).

La distinction entre l'interface d'un objet et ses détails de fonctionnement interne est importante pour deux raisons. D'abord, avoir une petite interface bien décrite rend un objet plus facile à utiliser. Il suffit de garder l'interface en tête, sans plus se préoccuper du reste, à moins d'avoir à changer l'objet lui-même.

Ensuite, il arrive régulièrement d'avoir à changer quelque chose dans le fonctionnement interne d'un type(14) d'objet, pour le rendre plus efficace par exemple, ou pour corriger un problème. Si le code extérieur a accès à toutes les propriétés d'un objet, il est difficile de changer le moindre détail sans avoir à mettre à jour tout le reste du code. Si le code extérieur utilise une petite interface, vous pouvez changer le fonctionnement interne de l'objet comme bon vous semble, tant que l'interface ne change pas.

Certaines personnes vont assez loin avec ce concept. Ils n'incluent, par exemple, aucune propriété dans l'interface d'un objet, et n'y autorisent que des méthodes - si leur type d'objet a une longueur, elle sera accessible via une méthode getLength, et pas directement comme une propriété length. De cette manière, si jamais ils décident de modifier leur objet de telle manière qu'il n'a plus de propriété length, par exemple parce que la longueur à retourner devient celle d'un tableau, ils peuvent mettre la fonction à jour, sans changer l'interface.

D'après mon point de vue personnel, dans la plupart des cas cela n'en vaut pas la peine. Ajouter une méthode getLength ne contenant que return this.length; est essentiellement un ajout de code inutile. En règle générale, je considère le code inutile plus problématique que la nécessité de modifier de temps à autre l'interface de mes objets.

Ajouter de nouvelles méthodes à des prototypes existants peut être très utile. En particulier les prototypes de Array et String en JavaScript peuvent recevoir quelques méthodes basiques supplémentaires. Nous pouvons, par exemple, remplacer forEach et map par des méthodes sur les tableaux, et transformer la fonction chaineCommencePar que nous avons écrite au chapitre 4 en méthode sur les chaînes de caractères.

De plus, si votre programme doit fonctionner sur la même page Web qu'un autre programme (qu'il soit de vous ou non) qui utilise naïvement for/in - comme nous l'avons vu jusque-là - alors ajouter des choses aux prototypes, et précisément à ceux d'Object et Array aura toutes les chances de casser quelque chose, vu que ces boucles vont d'un coup se mettre à inclure les nouvelles propriétés. Du coup, certaines personnes préfèrent ne pas toucher du tout à ces prototypes. Mais bien sûr, si vous êtes prudent, et qu'il n'y a aucune raison que votre code cohabite avec un code mal écrit, ajouter des méthodes aux prototypes standards est une très bonne technique.

Dans ce chapitre, nous allons fabriquer un terrarium virtuel, une boîte en verre avec des insectes vivant dedans. Ça impliquera de jouer avec des objets (ce qui tombe assez bien vu le nom du chapitre). Nous allons adopter une approche assez simple, en modélisant le terrarium par une grille à deux dimensions, comme la deuxième carte du chapitre 7. Sur cette grille, il y a un certain nombre de bestioles. Quand le terrarium est actif, toutes les bébêtes ont une opportunité d'agir (comme d'effectuer un déplacement) toutes les demi-secondes.

Du coup, on découpe l'espace et le temps en unités de taille fixe - des cases pour l'espace et des demi-secondes pour le temps. Ça rend généralement les choses plus simples à modéliser dans un programme, mais ça a bien sûr l'inconvénient d'être largement imprécis. Heureusement, ce simulateur de terrarium n'a pas besoin d'être précis et nous pouvons donc faire avec.

Un terrarium peut être représenté comme un « plan », défini comme étant un tableau de chaînes de caractères. Nous aurions pu n'utiliser qu'une seule chaîne de caractères, mais comme les chaînes de caractères JavaScript ne doivent comporter qu'une seule ligne, ça aurait été beaucoup plus compliqué à taper.

 
Sélectionnez
var lePlan =
  ["############################",
   "#      #    #      o      ##",
   "#                          #",
   "#          #####           #",
   "##         #   #    ##     #",
   "###           ##     #     #",
   "#           ###      #     #",
   "#   ####                   #",
   "#   ##       o             #",
   "# o  #         o       ### #",
   "#    #                     #",
   "############################"];

Les caractères "#" sont utilisés pour représenter les murs du terrarium (et les éléments de décors, comme les rochers au sol), les "o" représentent les bêtes et les espaces, comme vous vous en êtes sûrement douté, représentent les espaces vides.

Un plan-tableau de ce type est approprié pour représenter un objet terrarium. Cet objet garde trace de la forme et du contenu du terrarium, et permet aux insectes à l'intérieur de bouger. Il a quatre méthodes : tout d'abord toString, qui convertit le terrarium en une chaîne de caractères affichable, permettant d'avoir un aperçu de ce qui se passe dedans. Ensuite, il y a step, qui permet à toutes les bêtes du terrarium de se déplacer d'une case si elles le veulent. Et enfin il y a start et stop, qui contrôlent l'activité du terrarium. Lorsqu'il fonctionne, step est appelé automatiquement toutes les demi-secondes, et donc les insectes se déplacent.

Ex. 8.1

Les points sur la grille représenteront également des objets. Dans le chapitre 7 nous avons utilisé trois fonctions : point, ajouterPoints et pointsIdentiques pour travailler avec les points. Cette fois, nous utiliserons un constructeur et deux méthodes. Écrivez le constructeur Point, qui prend deux arguments, les coordonnées x et y du point, et produit un objet avec des propriétés x et y. Ajoutez au prototype de ce constructeur une méthode add, qui prend un autre point en argument et retourne un nouveau point dont les x et y sont la somme des x et y des deux points donnés. Ajoutez également une méthode isEqualTo, qui prend un point et renvoie un booléen, indiquant si le point local (this) a les mêmes coordonnées que le point donné.

En dehors des deux méthodes, les propriétés x et y font également partie de l'interface de ce type d'objets : le code utilisant des objets de type point pourra lire et modifier librement les x et y.

 
Sélectionnez
function Point(x, y) {
  this.x = x;
  this.y = y;
}
Point.prototype.add = function(autre) {
  return new Point(this.x + autre.x, this.y + autre.y);
};
Point.prototype.isEqualTo = function(autre) {
  return this.x == autre.x && this.y == autre.y;
};
 
show((new Point(3, 1)).add(new Point(2, 4)));

Assurez-vous que votre version de add laisse le point local (this) intact et produit bien un nouvel objet Point. Une méthode qui change le point courant serait similaire à l'opérateur +=, alors qu'on le veut équivalent à l'opérateur +.

Quand on écrit des objets pour développer un programme, on ne sait pas toujours quelle fonctionnalité va où. Certaines choses sont mieux implémentées sous forme de méthodes de l'objet, d'autres mieux rangées dans des fonctions et d'autres encore mieux modélisées par de nouveaux types d'objets. Pour garder l'organisation limpide, il est important de garder le nombre de méthodes et de responsabilités des objets aussi petit que possible. Quand un objet en fait trop, il devient un gros bazar de fonctionnalités et une formidable source de confusion.

J'ai dit plus haut que l'objet terrarium serait responsable du stockage de son contenu et de permettre aux insectes de bouger. Tout d'abord, précisons qu'il ne fait que permettre aux insectes de bouger. Les bébêtes seront elles-mêmes des objets, et ces objets seront responsables de leurs propres décisions. Le terrarium ne fournit en gros que l'infrastructure qui leur demande quoi faire chaque demi-seconde. Et s'ils décident de bouger, il s'assure que ça se fasse.

Stocker la grille sur laquelle le contenu du terrarium prend place peut vite se compliquer. Il faut définir une représentation, des moyens d'accéder à cette représentation, initialiser la grille depuis le « plan » (fourni sous forme de tableau) et restituer le contenu de la grille sous la forme d'une chaîne de caractères pour la méthode toString, sans oublier le mouvement des insectes sur la grille.

Lorsque vous vous retrouvez à mélanger représentation de données et code spécifique à un problème donné dans un seul objet, c'est une bonne idée d'essayer de mettre la représentation des données dans un type d'objet séparé. Dans ce cas, nous avons besoin de représenter une grille de valeurs, j'ai donc écrit un type Grille, qui supporte les opérations dont ce terrarium aura besoin.

Pour stocker les valeurs de la grille, il y a deux options. L'une peut utiliser un tableau de tableaux :

 
Sélectionnez
var grille = [["0,0", "1,0", "2,0"],
             ["0,1", "1,1", "2,1"]];
show(grille[1][2]);

Ou alors les valeurs peuvent toutes être mises dans un seul tableau. Dans ce cas, on retrouve l'élément x/y en cherchant dans le tableau l'élément en position x + y * largeur, où largeur est la largeur de la grille.

 
Sélectionnez
var grille = ["0,0", "1,0", "2,0",
              "0,1", "1,1", "2,1"];
show(grille[2 + 1 * 3]);

J'ai choisi la seconde représentation, car elle simplifie l'initialisation du tableau. new Array(x) produit un nouveau tableau de longueur x, rempli de valeurs undefined (indéfinies).

 
Sélectionnez
function Grille(largeur, hauteur) {
  this.largeur = largeur;
  this.hauteur = hauteur;
  this.cellules = new Array(largeur * hauteur);
}
Grille.prototype.valeurEn = function(point) {
  return this.cellules[point.y * this.largeur + point.x];
};
Grille.prototype.ecritValeurEn = function(point, valeur) {
  this.cellules[point.y * this.largeur + point.x] = valeur;
};
Grille.prototype.estDedans = function(point) {
  return point.x >= 0 && point.y >= 0 &&
         point.x < this.largeur && point.y < this.hauteur;
};
Grille.prototype.deplaceElement = function(depuis, vers) {
  this.ecritValeurEn(vers, this.valeurEn(depuis));
  this.ecritValeurEn(depuis, undefined);
};

Ex. 8.2

Nous allons également avoir besoin de parcourir tous les éléments de la grille, pour trouver les insectes qui ont besoin de bouger, ou pour convertir l'ensemble en une chaîne de caractères. Pour simplifier la chose, nous pouvons utiliser une fonction de haut niveau qui prend une action en argument. Ajouter une méthode each au prototype de Grille, qui prend en argument une fonction à deux arguments. Elle appelle cette fonction pour chaque point de la grille, lui donnant l'objet point comment premier argument, et la valeur du point sur la grille comme deuxième argument.

Parcourir les points depuis 0, 0, une ligne à la fois, de manière à ce que le point 1, 0 soit parcouru avant 0, 1. Cela simplifiera l'écriture de la fonction toString du terrarium après. (Indice : mettre une boucle for pour la coordonnée x à l'intérieur de la boucle for de la coordonnée y.)

Il est conseillé de ne pas mettre son nez directement dans la propriété cellules de la grille, mais d'utiliser valeurEn, pour récupérer ces valeurs. De cette manière, si nous décidons (pour une raison ou pour une autre) d'utiliser une méthode différente pour stocker les valeurs, nous n'aurons qu'à réécrire la fonction valeurEn et ecritValeurEn, et les autres méthodes resteront intactes.

 
Sélectionnez
Grille.prototype.each = function(action) {
  for (var y = 0; y < this.hauteur; y++) {
    for (var x = 0; x < this.largeur; x++) {
      var point = new Point(x, y);
      action(point, this.valeurEn(point));
    }
  }
};

Enfin, pour tester l'objet grille :

 
Sélectionnez
var testGrille = new Grille(3, 2);
testGrille.ecritValeurEn(new Point(1, 0), "#");
testGrille.ecritValeurEn(new Point(1, 1), "o");
testGrille.each(function(point, valeur) {
  print(point.x, ",", point.y, ": ", valeur);
});

Avant d'écrire un nouveau constructeur Terrarium, nous devons être plus précis à propos de ces « objets insectes » qui évolueront à l'intérieur. Précédemment, j'ai dit que le terrarium demandera aux insectes quelle action ils veulent effectuer. Cela fonctionnera de la fonction suivante : chaque insecte aura une méthode agit qui, appelée, renverra une « action ». Une action est un objet doté d'une propriété type, nommant le type d'action que l'insecte souhaitera effectuer. Par exemple "déplacement". La plupart des actions porteront d'autres informations, par exemple la direction souhaitée par l'insecte qui voudra se déplacer.

Les insectes sont terriblement myopes, ils ne peuvent voir que les cases à côté d'eux sur la grille. Mais ils peuvent s'en servir pour déterminer leurs actions. Quand la méthode agit est appelée, il lui est fourni un objet avec des informations sur l'environnement de l'insecte en question. Il porte une propriété pour chacune des huit directions autour de l'insecte. La propriété indiquant ce qu'il y a au-dessus de l'insecte est appelée "n", pour nord, pour ce qu'il y a au-dessus à droite "ne", pour nord-est, et ainsi de suite. Pour savoir quelle direction explorer selon le nom de la direction, l'objet dictionnaire suivant sera utile :

 
Sélectionnez
var directions = new Dictionary(
  {"n":  new Point( 0, -1),
   "ne": new Point( 1, -1),
   "e":  new Point( 1,  0),
   "se": new Point( 1,  1),
   "s":  new Point( 0,  1),
   "so": new Point(-1,  1),
   "o":  new Point(-1,  0),
   "no": new Point(-1, -1)});
 
show(new Point(4, 4).add(directions.lookup("se")));

Quand un insecte décide de se déplacer, il indique dans quelle direction il veut aller en renvoyant un objet action dont la propriété direction nomme laquelle de ces directions. Nous pouvons programmer un insecte primitif et idiot qui va toujours vers le sud, « vers la lumière », comme ceci :

 
Sélectionnez
function InsecteStupide() {};
InsecteStupide.prototype.agit = function(alentours) {
  return {type: "déplacement", direction: "s"};
};

Nous pouvons maintenir construire le type d'objet Terrarium lui-même. Commençons par son constructeur, qui reçoit un plan (un tableau de chaînes) comme argument, et initialise son objet grille.

 
Sélectionnez
var mur = {};
 
function Terrarium(plan) {
  var grille = new Grille(plan[0].length, plan.length);
  for (var y = 0; y < plan.length; y++) {
    var ligne = plan[y];
    for (var x = 0; x < ligne.length; x++) {
      grille.ecritValeurEn(new Point(x, y),
                      elementdApresCaractere(ligne.charAt(x)));
    }
  }
  this. grille= grille;
}
 
function elementdApresCaractere(caractere) {
  if (caractere == " ")
    return undefined;
  else if (caractere == "#")
    return mur;
  else if (caractere == "o")
    return new InsecteStupide();
}

mur est un objet utilisé pour repérer la position des murs sur la grille. Comme un vrai mur, il ne fait pas grand-chose, juste être quelque part et occuper une partie de l'espace.

La méthode la plus évidente de l'objet terrarium est toString, qui transforme un terrarium en chaîne de caractères. Pour faciliter cette tâche, nous donnons à mur et au prototype de InsecteStupide une propriété caractere, contenant la représentation sous forme de caractère de ceux-ci.

 
Sélectionnez
mur.caractere = "#";
InsecteStupide.prototype.caractere = "o";
 
function caracteredApresElement(element) {
  if (element == undefined)
    return " ";
  else
    return element.caractere;
}
 
show(caracteredApresElement(mur));

Ex. 8.3

Maintenant, nous pouvons utiliser la méthode each de l'objet Grille pour construire une chaîne de caractères. Pour que le résultat soit lisible, il est préférable d'avoir un retour chariot à chaque ligne. La coordonnée x de chaque case de la grille sera utilisée pour déterminer si la fin d'une ligne est atteinte. En ajoutant une méthode toString qui ne prend pas d'argument et renvoie une chaîne de caractères, et en passant cette chaîne à print, nous obtenons une vue bidimensionnelle convenable du terrarium.

 
Sélectionnez
Terrarium.prototype.toString = function() {
  var caracteres = [];
  var finDeLigne = this.grille.largeur - 1;
  this.grille.each(function(point, valeur) {
    caracteres.push(caracteredApresElement(valeur));
    if (point.x == finDeLigne)
      caracteres.push("\n");
  });
  return caracteres.join("");
};

Et pour l'essayer...

 
Sélectionnez
var terrarium = new Terrarium(lePlan);
print(terrarium.toString());

Il est possible qu'en essayant de résoudre l'exercice précédent, vous ayez voulu accéder à this.grille dans le corps de la fonction passée en argument de la méthode each de l'objet grille. Cela ne peut pas fonctionner, car l'appel à une fonction a pour conséquence qu'à l'intérieur de cette fonction, this prend une nouvelle valeur, même si elle n'est pas utilisée en tant que méthode. Ainsi, aucune variable this à l'extérieur d'une fonction ne peut être visible.

Parfois, il est nécessaire de contourner ceci en stockant les informations dont on a besoin dans une variable, par exemple finDeLigne, qui elle est visible dans la fonction imbriquée. Si vous avez besoin d'accéder à la variable this d'un objet, vous pouvez la stocker dans une autre variable. Le nom self (ou that) est souvent utilisé pour une telle variable.

Mais l'utilisation de ces variables en plus peut être source de confusion. Une autre bonne solution est d'utiliser une fonction proche de partial décrite dans le chapitre 6. Au lieu d'ajouter un argument à la fonction, celle-ci passe l'objet this, par l'intermédiaire du premier argument de la méthode apply dont disposent toutes les fonctions :

 
Sélectionnez
function bind(func, objet) {
  return function(){
    return func.apply(objet, arguments);
  };
}
 
var tableauTest = [];
var ajouterDansTest = bind(tableauTest.push, tableauTest);
ajouterDansTest("A");
ajouterDansTest("B");
show(tableauTest);

De cette façon, vous pouvez lier la variable this d'une fonction imbriquée à la variable this de la fonction appelante, les deux this seront identiques.

Ex. 8.4

Dans l'expression bind(tableauTest.push, tableauTest) le nom tableauTest est encore utilisé deux fois. Pouvez-vous concevoir une fonction method, qui permet de lier un objet à une de ses méthodes sans nommer deux fois l'objet ?

Il est possible de passer à un objet une chaîne de caractères contenant le nom d'une de ses méthodes. De cette façon, la fonction method peut connaître le nom de la fonction à appliquer à l'objet.

 
Sélectionnez
function method(objet, nom) {
  return function() {
    objet[nom].apply(objet, arguments);
  };
}
 
var ajouterDansTest = method(tableauTest, "push");

Nous aurons besoin de bind (ou method) quand nous écrirons la méthode step de l'objet terrarium. Cette méthode devra parcourir tous les insectes de la grille, en leur demandant quelle action ils veulent effectuer, et en effectuant pour eux cette action. Vous pourriez être tenté d'utiliser each sur l'objet grille, et traiter les insectes un par un au fur et à mesure que vous les rencontrez. Mais ce faisant, si un insecte se déplaçait vers le sud ou l'est, vous le rencontreriez à nouveau dans le même tour, et il serait à nouveau déplacé.

À la place, nous allons extraire tous les insectes vers un tableau, et partant de là, les traiter un par un. La méthode ci-dessous extrait les insectes, et même tout objet qui possède une méthode agit, et enregistre ces objets, et leurs positions respectives avant déplacement, dans un tableau d'objets.

 
Sélectionnez
Terrarium.prototype.listeCreaturesEnAction = function() {
  var trouves = [];
  this.grille.each(function(point, valeur) {
    if (valeur != undefined && valeur.agit)
      trouves.push({object: valeur, point: point});
  });
  return trouves;
};

Ex. 8.5

Lorsque l'on demande à un insecte quel déplacement il veut faire, il faut lui passer un objet lui décrivant les cases alentour. Cet objet utilisera les noms de direction que nous avons vus précédemment ("n", "ne", etc.) comme noms de propriétés. Chaque propriété contiendra une chaîne d'un caractère tel que renvoyé par caracteredApresElement, indiquant ce que peut voir l'insecte dans cette direction.

Ajouter une méthode listeAlentours au prototype de Terrarium. Elle prend un argument, le point où l'insecte se trouve, et renvoie un objet décrivant l'entourage de ce point. Quand un point se trouve à une bordure de la grille, utiliser "#" pour les directions qui débordent de la grille, ainsi l'insecte ne pourra s'y rendre.

Conseil : ne pas décrire chacune des directions, mais utiliser la méthode each sur le dictionnaire directions.

 
Sélectionnez
Terrarium.prototype.listeAlentours = function(centre) {
  var resultat = {};
  var grille = this.grille;
  directions.each(function(nom, direction) {
    var place = centre.add(direction);
    if (grille.estDedans(place))
      resultat[nom] = caracteredApresElement(grille.valeurEn(place));
    else
      resultat[nom] = "#";
  });
  return resultat;
};

Remarquez l'utilisation de la variable grille pour passer outre les difficultés liées à l'usage de this.

Les deux méthodes ci-dessus ne font pas partie de l'interface externe de l'objet Terrarium, mais sont des détails internes à l'objet. Certains langages de programmation permettent de déclarer explicitement certaines méthodes et propriétés comme 'privées', et provoquent une erreur si on accède à celles-ci en dehors de l'objet. Ce n'est pas le cas de JavaScript, c'est pourquoi vous pourriez utiliser des commentaires pour décrire l'interface d'un objet. Parfois il est utile d'utiliser des conventions de nommage pour distinguer les propriétés externes et internes, par exemple en préfixant les propriétés internes avec un caractère souligné ('_'). Cela permet de repérer plus facilement les utilisations accidentelles des propriétés qui ne font pas partie de l'interface des objets.

Voici encore une méthode interne, celle qui va demander à un insecte ce qu'il veut faire, et l'effectuer. Elle prend en argument un objet avec les propriétés object et point, comme le renvoie listeCreaturesEnAction. Pour le moment, elle ne reconnait que l'action "déplacement" :

 
Sélectionnez
Terrarium.prototype.actionnerUneCreature = function(creature) {
  var alentours = this.listeAlentours(creature.point);
  var action = creature.object.agit(alentours);
  if (action.type == "déplacement" && directions.contains(action.direction)) {
    var to = creature.point.add(directions.lookup(action.direction));
    if (this.grille.estDedans(to) && this.grille.valeurEn(to) == undefined)
      this.grille.deplaceElement(creature.point, to);
  }
  else {
    throw new Error("Action invalide : " + action.type);
  }
};

Remarquez que la méthode vérifie si la direction choisie amène bien à une case vide, dans le cas contraire, la méthode ignore le déplacement. De cette façon, les insectes peuvent bien demander tout ce qu'ils veulent, l'action ne sera effectuée que si elle est possible. Ce mécanisme agit comme une couche d'isolation entre les insectes et le terrarium, et nous autorise quelques approximations dans l'écriture des méthodes agit des insectes : par exemple InsecteStupide ne se déplace que vers le sud, sans se demander si un mur se trouve sur son chemin.

Ces trois méthodes internes vont nous permettre enfin d'écrire la méthode step, qui permettra aux insectes de faire quelque chose (et même tout élément doté d'une méthode agit - nous pourrions tout aussi bien donner une telle méthode à l'objet mur et les murs se déplaceraient).

 
Sélectionnez
Terrarium.prototype.step = function() {
  forEach(this.listeCreaturesEnAction(),
          bind(this.actionnerUneCreature, this));
};

Maintenant, construisons un terrarium et voyons les insectes se déplacer.

 
Sélectionnez
var terrarium = new Terrarium(lePlan);
print(terrarium);
terrarium.step();
print(terrarium);

Examinons un instant l'instruction ci-dessus print(terrarium), comment fait-elle pour renvoyer le contenu de notre méthode toString ? print transfome les arguments qui lui sont passés en chaîne de caractères, en utilisant la fonction String. Les objets sont transformés en chaîne de caractères par l'appel de leur méthode toString, aussi, écrire une méthode toString dans nos propres objets est un bon moyen de les rendre lisibles lors de l'appel de print.

 
Sélectionnez
Point.prototype.toString = function() {
  return "(" + this.x + "," + this.y + ")";
};
print(new Point(5, 5));

Comme prévu, l'objet Terrarium sera doté de méthode start et stop pour démarrer et arrêter la simulation. Pour cela, nous utiliserons deux fonctions fournies par le navigateur Web, appelées setInterval et clearInterval. La première est utilisée dans le but que son premier argument (une fonction ou une chaîne de caractères contenant du code JavaScript) soit exécuté périodiquement. Son deuxième argument est la durée en millisecondes (1/1000 de seconde) entre les exécutions. La fonction renvoie une valeur qui pourra servir d'argument à clearInterval pour arrêter les exécutions périodiques.

 
Sélectionnez
var pénible = setInterval(function() {print("Quoi?");}, 400);

Et...

 
Sélectionnez
clearInterval(pénible);

Il existe des fonctions proches pour exécuter une action une seule fois après un laps de temps. setTimeout exécute une fonction ou une chaine de caractères après un délai exprimé en millisecondes, et clearTimeout permet d'annuler une telle action.

 
Sélectionnez
Terrarium.prototype.start = function() {
  if (!this.running)
    this.running = setInterval(bind(this.step, this), 500);
};
 
Terrarium.prototype.stop = function() {
  if (this.running) {
    clearInterval(this.running);
    this.running = null;
  }
};

À ce stade, nous avons un terrarium avec des insectes très simplistes, que nous pouvons faire fonctionner. Mais pour voir ce qu'il s'y passe, il nous faut constamment exécuter print(terrarium). Ce n'est pas très pratique. Ce serait agréable que le contenu s'affiche automatiquement. Ce serait encore mieux si, au lieu d'afficher par milliers les images successives des terraria, nous n'ayons qu'une seule image que nous mettrions à jour. Pour ce dernier problème, cette page offre une fonction nommée inPlacePrinter. Elle renvoie une fonction comme print qui, au lieu d'effectuer un nouvel affichage, remplace l'affichage précédent.

 
Sélectionnez
var printHere = inPlacePrinter();
printHere("Actuellement vous voyez ceci.");
setTimeout(partial(printHere, "Plus maintenant."), 1000);

Pour que le terrarium s'affiche à chaque changement, nous modifions la méthode step comme suit :

 
Sélectionnez
Terrarium.prototype.step = function() {
  forEach(this.listeCreaturesEnAction(),
          bind(this.actionnerUneCreature, this));
  if (this.onStep)
    this.onStep();
};

En faisant cela, si une propriété onStep est présente dans l'objet terrarium, elle est appelée à chaque étape.

 
Sélectionnez
var terrarium = new Terrarium(lePlan);
terrarium.onStep = partial(inPlacePrinter(), terrarium);
terrarium.start();

Remarquez l'utilisation de partial. Cette méthode partial renvoie une fonction d'affichage appliquée à l'objet terrarium. La fonction d'affichage ne demandant qu'un seul argument, après application partielle, nous obtenons une fonction sans argument. C'est exactement ce dont nous avons besoin pour la propriété onStep.

N'oubliez pas d'arrêter la simulation du terrarium, quand il perd de son intérêt (ce qui ne devrait pas tarder), pour éviter de consommer les ressources de votre ordinateur inutilement :

 
Sélectionnez
terrarium.stop();

Qui voudrait d'une simulation de terrarium avec une seule sorte d'insecte, stupide qui plus est ? Pas moi. Ce serait judicieux si nous pouvions ajouter différentes sortes d'insectes. Heureusement, il nous suffit pour cela de rendre la fonction elementdApresCaractere plus générale. Pour le moment, elle décrit trois possibilités « codées en dur », c'est-à-dire de façon linéaire et sans flexibilité :

 
Sélectionnez
function elementdApresCaractere(caractere) {
  if (caractere == " ")
    return undefined;
  else if (caractere == "#")
    return mur;
  else if (caractere == "o")
    return new InsecteStupide();
}

Les deux premiers cas restent tels quels, le dernier étant trop spécifique. Une meilleure approche serait de stocker les constructeurs des objets insectes et les caractères qui leur correspondent dans un dictionnaire, et de rechercher dans ce dictionnaire ces caractères :

 
Sélectionnez
var typesDeCreature = new Dictionary();
typesDeCreature.enregistre = function(constructeurDeInsecte) {
  this.store(constructeurDeInsecte.prototype.caractere, constructeurDeInsecte);
};
 
function elementdApresCaractere(caractere) {
  if (caractere == " ")
    return undefined;
  else if (caractere == "#")
    return mur;
  else if (typesDeCreature.contains(caractere))
    return new (typesDeCreature.lookup(caractere))();
  else
    throw new Error("Caractère inconnu: " + caractere);
}

Remarquez qu'une méthode enregistre est ajoutée à l'objet typesDeCreature - celui-ci est de type dictionnaire, ce qui n'empêche en rien de lui ajouter une méthode. Cette fonction extrait le caractère associé au constructeur de l'insecte, et stocke ce caractère dans le dictionnaire. Cette méthode ne doit être appelée que sur des objets dont le prototype possède une propriété caractere.

La fonction elementdApresCaractere est modifiée pour rechercher le caractère présent dans typesDeCreature, et provoque une exception si elle tombe sur un caractère inconnu.

Voici une nouvelle sorte d'insecte, ainsi que les instructions pour enregistrer son caractère dans typesDeCreature :

 
Sélectionnez
function InsecteaRebond() {
  this.direction = "ne";
}
InsecteaRebond.prototype.agit = function(alentours) {
  if (alentours[this.direction] != " ")
    this.direction = (this.direction == "ne" ? "so" : "ne");
  return {type: "déplacement", direction: this.direction};
};
InsecteaRebond.prototype.caractere = "%";
 
typesDeCreature.enregistre(InsecteaRebond);

Pouvez-vous comprendre ce qu'il fait ?

Ex. 8.6

Créer un insecte nommé InsecteIvre qui essaie de se déplacer dans une direction quelconque à chaque tour, peu importe s'il y a un mur en face de lui. Rappelez-vous le fonctionnement de Math.random dans le chapitre 7.

Pour déterminer une direction de façon aléatoire, nous avons besoin d'un tableau avec la liste des directions. Nous pourrions juste écrire un tableau de cette façon : ["n", "ne"...], mais cela dupliquerait des informations, et les duplications d'information me rendent nerveux. Nous pourrions également utiliser la méthode each sur le dictionnaire directions pour construire un nouveau tableau, ce serait déjà mieux.

Mais vous devez comprendre qu'il y a, ici, une façon bien plus générale de procéder. Récupérer la liste des noms de propriété d'un dictionnaire est un outil utile, aussi, ajoutons-le au prototype de l'objet Dictionary.

 
Sélectionnez
Dictionary.prototype.names = function() {
  var noms = [];
  this.each(function(nom, valeur) {noms.push(nom);});
  return noms;
};
 
show(directions.names());

Un programmeur vraiment névrosé voudrait immédiatement rétablir la symétrie en ajoutant une méthode values qui retournerait la liste des valeurs d'un dictionnaire. Mais je suppose que nous pouvons attendre d'en avoir vraiment besoin.

Voici une façon de prendre un élément d'un tableau au hasard :

 
Sélectionnez
function elementAuHasard(tableau) {
  if (tableau.length == 0)
    throw new Error("Le tableau est vide.");
  return tableau[Math.floor(Math.random() * tableau.length)];
}
 
show(elementAuHasard(["face", "pile"]));

Et l'insecte lui-même :

 
Sélectionnez
function InsecteIvre() {};
InsecteIvre.prototype.agit = function(alentours) {
  return {type: "déplacement",
          direction: elementAuHasard(directions.names())};
};
InsecteIvre.prototype.caractere = "~";
 
typesDeCreature.enregistre(InsecteIvre);

Essayons maintenant ces nouveaux insectes :

 
Sélectionnez
var nouveauPlan =
  ["############################",
   "#                      #####",
   "#    ##                 ####",
   "#   ####     ~ ~          ##",
   "#    ##       ~            #",
   "#                          #",
   "#                ###       #",
   "#               #####      #",
   "#                ###       #",
   "# %        ###        %    #",
   "#        #######           #",
   "############################"];
 
var terrarium = new Terrarium(nouveauPlan);
terrarium.onStep = partial(inPlacePrinter(), terrarium);
terrarium.start();

Vous voyez comment les insectes à rebond rebondissent sur les insectes en état d'ébriété ? Dramatique. De toute façon, quand vous en aurez assez de regarder ce spectacle fascinant, vous pourrez y mettre fin :

 
Sélectionnez
terrarium.stop();

Nous avons maintenant deux sortes d'objets possédant chacun une méthode agit et une propriété caractere. Comme ils partagent ces caractéristiques, le terrarium peut dialoguer avec eux d'une façon commune. Ceci nous autorise à avoir toutes sortes d'insectes, sans rien changer au code de l'objet terrarium. Cette technique est appelée polymorphisme, et c'est sûrement l'un des aspects les plus puissants de la programmation orientée objet.

L'idée de base du polymorphisme est que lorsqu'un morceau de programme est écrit pour manipuler des objets ayant une certaine interface, n'importe quel objet qui présente cette interface pourra être raccordé à ce morceau de programme, et le tout fonctionne. Nous avons déjà vu un exemple de cela, à savoir la méthode toString de nombreux objets. Tous les objets ayant une méthode toString pertinente peuvent être passés à la fonction print, ou toute autre fonction qui aura besoin de convertir un objet en chaîne de caractères, peu importe la façon dont cette dernière est produite.

De la même façon, forEach travaille sur de véritables objets tableau ou sur des objets similaires aux tableaux, forEach recevant cet objet tableau dans sa variable arguments, car tout ce dont cette fonction a besoin, ce sont des propriétés numérotées 0.

Pour rendre la vie dans le terrarium plus réelle, nous allons y ajouter les concepts de nourriture et de reproduction. Chaque créature vivante du terrarium reçoit une nouvelle propriété, énergie, qui est diminuée lorsqu'elle effectue une action, et augmentée lorsqu'elle mange quelque chose. Lorsqu'elle a suffisamment d'énergie, une chose vivante peut se reproduire(15), engendrant une nouvelle créature du même type.

S'il n'y avait que des insectes, les dépenses d'énergie de leurs déplacements, et le fait qu'ils se mangeraient entre eux feraient que notre terrarium succomberait rapidement sous l'effet de l'entropie, serait à court d'énergie, et deviendrait un lieu abandonné et sans vie. Pour empêcher que ceci se produise (au moins, que cela ne se produise pas trop vite), nous ajoutons du lichen au terrarium. Les lichens ne se déplacent pas, ils utilisent la photosynthèse pour produire de l'énergie et se reproduire.

Pour que cela fonctionne, nous aurons besoin d'un terrarium avec une méthode actionnerUneCreature différente. Nous pourrions simplement changer cette méthode dans le prototype de Terrarium, mais nous sommes très attachés à la simulation des insectes sauteurs et des insectes en état d'ébriété, et ne voulons pas casser ce premier terrarium.

Ce que nous pouvons faire est écrire un nouveau constructeur, TerrariumPlusVivant, dont le prototype est basé sur le prototype de Terrarium, mais qui possède une méthode actionnerUneCreature différente.

Il existe plusieurs façons de faire cela. Nous pourrions énumérer les propriétés de Terrarium.prototype, et les ajouter une à une dans TerrariumPlusVivant.prototype. Ce serait simple à faire, et dans certains cas la meilleure solution. Mais ici nous avons une façon plus propre de faire. Si nous faisons du prototype du premier objet terrarium le prototype du nouveau terrarium (prenez le temps de bien comprendre cette phrase), ce nouveau Terrarium en aurait toutes les propriétés.

Malheureusement, JavaScript ne propose pas de moyen direct de créer un objet dont le prototype est celui d'un autre objet. Il est possible d'écrire une fonction qui fait cela, en utilisant l'astuce suivante :

 
Sélectionnez
function clone(objet) {
  function ConstructeurNouveauPourChaqueClone(){}
  ConstructeurNouveauPourChaqueClone.prototype = objet;
  return new ConstructeurNouveauPourChaqueClone();
}

Cette fonction clone déclare un constructeur nommé ConstructeurNouveauPourChaqueClone qui est vide et unique, dont le prototype est l'objet passé en argument. En appelant new sur ce constructeur, un nouvel objet est créé, basé sur l'objet passé en argument.

 
Sélectionnez
function TerrariumPlusVivant(plan) {
  Terrarium.call(this, plan);
}
TerrariumPlusVivant.prototype = clone(Terrarium.prototype);
TerrariumPlusVivant.prototype.constructor = TerrariumPlusVivant;

Le nouveau constructeur n'a pas besoin de faire quoi que ce soit de plus que l'ancien, donc il se contente d'appeler l'ancien sur l'objet this. Il nous faut également restaurer la propriété constructor du nouveau prototype, sinon il clamerait que son constructeur est Terrarium (ce qui, bien sûr, n'est un problème que si on se sert de cette propriété, ce qui n'est pas notre cas).

Il est maintenant possible de remplacer certaines méthodes de l'objet TerrariumPlusVivant, et d'en ajouter d'autres. Nous avons un type d'objet basé sur un autre, ce qui nous épargne le travail de réécrire toutes les méthodes communes à Terrarium et TerrariumPlusVivant. Cette technique est appelée « héritage ». Le nouveau type hérite des propriétés de l'ancien type. Dans la plupart des cas, cela signifie que le nouveau type possédera toujours l'interface de l'ancien, bien qu'il puisse posséder des méthodes en plus, que l'ancien n'a pas. De cette façon, les objets du nouveau type pourraient prendre la place (selon le polymorphisme) des objets de l'ancien type.

Dans les langages de programmation avec un support explicite de l'orientation objet, l'héritage est une chose très simple à mettre en œuvre. JavaScript n'a pas de moyen simple de le faire. À cause de cela, les programmeurs en JavaScript ont inventé différentes approches pour le faire. Malheureusement, aucune d'entre elles n'est parfaite.

À la fin de ce chapitre, je vous montrerai d'autres façons de mettre en œuvre l'héritage, et leurs inconvénients.

Voici une nouvelle méthode actionnerUneCreature. Elle est volumineuse :

 
Sélectionnez
TerrariumPlusVivant.prototype.actionnerUneCreature = function(creature) {
  var alentours = this.listeAlentours(creature.point);
  var action = creature.object.agit(alentours);
 
  var cible = undefined;
  var elementDansCible = undefined;
  if (action.direction && directions.contains(action.direction)) {
    var direction = directions.lookup(action.direction);
    var directionSouhaitee = creature.point.add(direction);
    if (this.grille.estDedans(directionSouhaitee )) {
      cible = directionSouhaitee ;
      elementDansCible = this.grille.valeurEn(cible);
    }
  }
 
  if (action.type == "déplacement") {
    if (cible && !elementDansCible) {
      this.grille.deplaceElement(creature.point, cible);
      creature.point = cible;
      creature.object.energie -= 1;
    }
  }
  else if (action.type == "manger") {
    if (elementDansCible && elementDansCible.energie) {
      this.grille.ecritValeurEn(cible, undefined);
      creature.object.energie += elementDansCible.energie;
    }
  }
  else if (action.type == "photosynthese") {
    creature.object.energie += 1;
  }
  else if (action.type == "reproduction") {
    if (cible && !elementDansCible) {
      var espece = caracteredApresElement(creature.object);
      var nouvelleCreature = elementdApresCaractere(espece);
      //la créature parente perd 2 fois la quantité d'énergie de la créature naissante
      creature.object.energie -= nouvelleCreature.energie * 2;
      if (creature.object.energie > 0)
        this.grille.ecritValeurEn(cible, nouvelleCreature);
    }
  }
  else if (action.type == "attente") {
    creature.object.energie -= 0.2;
  }
  else {
    throw new Error("Action invalide : " + action.type);
  }
 
  if (creature.object.energie <= 0)
    this.grille.ecritValeurEn(creature.point, undefined);
};

La fonction commence toujours par interroger les créatures pour une action. Ensuite, si l'action possède une propriété direction, la fonction détermine immédiatement à quel endroit de la grille cette direction amène, et ce qu'il y a à cet endroit. Trois des cinq actions implantées dans notre simulation ont besoin de savoir cela, et le code serait encore plus difficile à comprendre si ces calculs étaient faits à part. Si l'action n'a pas de propriété direction, ou si celle-ci est incorrecte, les variables cible et elementDansCible restent à leur valeur undefined.

Après cela, toutes les actions sont passées en revue. Certaines actions demandent des vérifications supplémentaires avant leur exécution, ce qui est fait en utilisant un if distinct pour que si une créature cherche, par exemple, à passer à travers un mur, une exception "Action invalide" ne soit pas générée.

Remarquez que dans l'action "reproduction", la créature parente perd deux fois la quantité d'énergie reçue par la nouvelle créature (la procréation n'est pas une chose facile), et la nouvelle créature n'est placée sur la grille que si son parent a suffisant d'énergie pour l'engendrer.

Après qu'une action a été effectuée, nous regardons si la créature a encore de l'énergie. Si elle n'en a plus, elle meurt, et nous la supprimons.

Le lichen n'est pas un organisme très complexe. Nous allons utiliser le caractère "*" pour le représenter. Vérifiez que vous avez bien défini la fonction elementAuHasard pour l'exercice 8.6, car elle sera utilisée de nouveau ici.

 
Sélectionnez
function Lichen() {
  this.energie = 5;
}
Lichen.prototype.agit = function(alentours) {
  var espaceVide = trouverDirections(alentours, " ");
  if (this.energie >= 13 && espaceVide.length > 0)
    return {type: "reproduction", direction: elementAuHasard(espaceVide)};
  else if (this.energie < 20)
    return {type: "photosynthese"};
  else
    return {type: "attente"};
};
Lichen.prototype.caractere = "*";
 
typesDeCreature.enregistre(Lichen);
 
function trouverDirections(alentours, directionSouhaite) {
  var trouve = [];
  directions.each(function(name) {
    if (alentours[name] == directionSouhaite)
      trouve.push(name);
  });
  return trouve;
}

Les lichens ne grandissent jamais au-delà de 20 unités d'énergie, sinon ils seraient trop imposants, quand, encerclés par d'autres lichens, ils n'ont plus de place pour se reproduire.

Ex. 8.7

Créez une créature dévoreuse de lichens, MangeuseLichen. Elle commence avec 10 unités d'énergie, et agit de la façon suivante :

  • Quand elle a 30 ou plus d'énergie et une case vide près d'elle, elle se reproduit ;
  • Sinon, s'il y a du lichen près d'elle, elle en mange un, choisi aléatoirement ;
  • Sinon, s'il y a la place de se bouger, elle va vers une case vide aléatoire ;
  • Sinon elle attend.

Utilisez les fonctions trouverDirections et elementAuHasard pour déterminer le contenu de l'entourage de la créature, et faire des choix aléatoires. Donnez à cette créature le caractère "c" (pour faire penser à pac-man).

 
Sélectionnez
function MangeuseLichen() {
  this.energie = 10;
}
MangeuseLichen.prototype.agit = function(alentours) {
  var espaceVide = trouverDirections(alentours, " ");
  var lichen = trouverDirections(alentours, "*");
 
  if (this.energie >= 30 && espaceVide.length > 0)
    return {type: "reproduction", direction: elementAuHasard(espaceVide)};
  else if (lichen.length > 0)
    return {type: "manger", direction: elementAuHasard(lichen)};
  else if (espaceVide.length > 0)
    return {type: "déplacement", direction: elementAuHasard(espaceVide)};
  else
    return {type: "attente"};
};
MangeuseLichen.prototype.caractere = "c";
 
typesDeCreature.enregistre(MangeuseLichen);

Et pour l'essayer.

 
Sélectionnez
var lichenPlan =
  ["############################",
   "#                     ######",
   "#    ***                **##",
   "#   *##**         **  c  *##",
   "#    ***     c    ##**    *#",
   "#       c         ##***   *#",
   "#                 ##**    *#",
   "#   c       #*            *#",
   "#*          #**       c   *#",
   "#***        ##**    c    **#",
   "#*****     ###***       *###",
   "############################"];
 
var terrarium = new TerrariumPlusVivant(lichenPlan);
terrarium.onStep = partial(inPlacePrinter(), terrarium);
terrarium.start();

La plupart du temps, vous devriez voir le lichen envahir rapidement le terrarium, cette abondance de nourriture provoquera une abondance de créatures voraces, si nombreuses qu'elles finiront par épuiser les ressources en lichen, et enfin s'épuiser elles-mêmes. La nature est si tragique.

 
Sélectionnez
terrarium.stop();

Constater que les occupants de votre terrarium disparaissent après quelques minutes est un peu déprimant. Pour y faire face, nous allons éduquer nos créatures dévoreuses de lichen au principe de l'agriculture raisonnée. En faisant qu'elles ne mangent du lichen que si elles sont à proximité de deux d'entre eux, quel que soit l'état de leur faim, elles ne pourront plus exterminer le lichen. Cela demande de la discipline, mais le résultat est un biotope qui ne s'autodétruit pas. Voici une nouvelle méthode agit - le seul changement est que l'action de manger ne se fait que si lichen.length est au moins égal à 2.

 
Sélectionnez
MangeuseLichen.prototype.agit = function(alentours) {
  var espaceVide = trouverDirections(alentours, " ");
  var lichen = trouverDirections(alentours, "*");
 
  if (this.energie >= 30 && espaceVide.length > 0)
    return {type: "reproduction", direction: elementAuHasard(espaceVide)};
  else if (lichen.length > 1)
    return {type: "manger", direction: elementAuHasard(lichen)};
  else if (espaceVide.length > 0)
    return {type: "déplacement", direction: elementAuHasard(espaceVide)};
  else
    return {type: "attente"};
};

Faites fonctionner la simulation du terrarium lichenPlan à nouveau et constatez son évolution. À moins d'être très chanceux, vous allez probablement constater l'extinction des créatures dévoreuses au bout d'un certain temps, parce que lorsque survient la famine, ces créatures se déplacent de façon désordonnée, au lieu de rechercher le lichen qui n'est pas très loin d'elles.

Ex. 8.8

Cherchez un moyen de rendre la créature MangeuseLichen plus apte à la survie. Ne trichez pas - une instruction this.energie += 100 serait de la triche. Si vous réécrivez le constructeur, n'oubliez pas de l'enregistrer à nouveau dans le dictionnaire typesDeCreature, sinon le terrarium continuerait d'utiliser l'ancien constructeur.

Une approche serait de restreindre le caractère aléatoire des déplacements. En choisissant systématiquement une direction aléatoire, elles reviennent très souvent sur leurs pas, sans rien trouver à manger. En se rappelant de la direction d'où elles viennent, et en privilégiant cette direction, elles dépenseraient moins de temps et trouveraient plus facilement de la nourriture.

 
Sélectionnez
function MangeuseLichenHabile() {
  this.energie = 10;
  this.direction = "ne";
}
MangeuseLichenHabile.prototype.agit = function(alentours) {
  var espaceVide = trouverDirections(alentours, " ");
  var lichen = trouverDirections(alentours, "*");
 
  if (this.energie >= 30 && espaceVide.length > 0) {
    return {type: "reproduction",
            direction: elementAuHasard(espaceVide)};
  }
  else if (lichen.length > 1) {
    return {type: "manger",
            direction: elementAuHasard(lichen)};
  }
  else if (espaceVide.length > 0) {
    if (alentours[this.direction] != " ")
      this.direction = elementAuHasard(espaceVide);
    return {type: "déplacement",
            direction: this.direction};
  }
  else {
    return {type: "attente"};
  }
};
MangeuseLichenHabile.prototype.caractere = "c";
 
typesDeCreature.enregistre(MangeuseLichenHabile);

Essayez-la avec le plan de terrarium précédent.

Ex. 8.9

Une chaîne alimentaire à un seul maillon est un peu rudimentaire. Pouvez-vous écrire une nouvelle créature, nommée MangeuseMangeuseLichen, (avec un caractère "@"), qui survit en mangeant des dévoreuses de lichens ? Trouver également un moyen pour cette nouvelle créature de s'intégrer dans l'écosystème sans qu'elles ne s'éteignent trop vite. Modifiez le tableau lichenPlan pour inclure quelques-unes d'entre elles, et essayez le tout.

C'est maintenant à vous de jouer, je n'ai pas trouvé de moyen véritablement efficace d'empêcher ces créatures de s'éteindre immédiatement ou d'engloutir toutes les dévoreuses de lichen, et de s'éteindre ensuite. L'astuce qui consiste à autoriser une créature à ne manger que lorsque deux unités de nourriture sont à proximité ne fonctionne pas très bien pour elles, car, leur nourriture étant souvent en déplacement, il est rare d'en trouver deux à proximité l'une de l'autre. Rendre les dévoreuses de dévoreuses très grasses (avec beaucoup d'énergie) à quelque efficacité, car elles peuvent survivre lorsque les dévoreuses de lichen se font rares et se reproduisent doucement, ce qui empêche une raréfaction trop rapide de leur nourriture.

Les lichens et les créatures qui les mangent sont dans un mouvement périodique - : parfois les lichens sont abondants, ce qui provoque beaucoup de naissances de mangeurs de lichen, ce qui provoque ensuite une rareté du lichen, puis la rareté des mangeurs de lichen, enfin le lichen prospère à nouveau, et ainsi de suite. Vous pouvez essayer de faire 'hiberner' les mangeurs de mangeurs de lichen (utiliser l'action "attente" un certain temps), quand ils n'ont rien à manger pour quelques tours. Une stratégie serait de trouver la bonne durée d'hibernation, en nombre de tours, ou de leur donner un moyen de se réveiller lorsqu'ils sentent beaucoup de nourriture.

Ceci termine notre discussion sur les terraria. Le reste de ce chapitre est dédié à une exploration en profondeur du concept d'héritage, et les problèmes liés à l'héritage en JavaScript.

Maintenant, un peu de théorie. Les étudiants qui abordent la programmation orientée objet sont souvent confrontés à des discussions longues et pleines de subtilités sur les façons correctes et incorrectes d'utiliser l'héritage. Il est important de garder à l'esprit qu'au bout du compte, l'héritage est un moyen pour des programmeurs paresseux(16) d'écrire moins de code. Ainsi, la question de savoir si l'héritage est correctement utilisé se résume à la question de savoir si le code produit fonctionne correctement et n'a pas de répétition inutile. Pour autant, les principes discutés par ces étudiants sont aussi une bonne façon d'aborder l'héritage.

L'héritage est la création de nouveaux types d'objet, les « sous-types », basés sur des types existants, les « super-types ». Le sous-type commence avec la totalité des propriétés et des méthodes du super-type, il hérite de lui, ensuite, il en modifie quelques-uns, éventuellement en ajoute. L'héritage est mieux utilisé quand les objets décrits par le sous-type peuvent être considérés comme étant également des objets du super-type.

Ainsi, un type Piano peut être un sous-type du type Instrument, parce qu'un piano est un instrument. Comme un piano a un tableau de touches, on peut être tenté de faire de Piano un sous-type de Array, mais un piano n'est pas un tableau de touches, et l'implémenter de cette façon entraînerait plein de choses idiotes. Par exemple, un piano a aussi des pédales. Pourquoi piano[0] me renvoie la première touche, et pas la première pédale ? La situation est que, évidemment, le piano possède des touches, il est donc meilleur de lui donner une propriété touches, et éventuellement une autre propriété pédales, ces deux propriétés étant des tableaux.

Il est possible pour un sous-type d'être le super-type d'un autre sous-type. Certains problèmes sont mieux résolus en construisant un arbre complexe de types. Prenez garde à ne pas être trop enthousiaste avec l'héritage. Une utilisation abusive de l'héritage est un bon moyen de transformer un programme en un bazar monstrueux.

Le fonctionnement du mot-clé new et la propriété prototype d'un constructeur suggèrent une certaine façon d'utiliser les objets. Pour des objets simples, comme les créatures du terrarium, cette façon fonctionne bien. Malheureusement, quand un programme utilise l'héritage de façon plus développée, cette approche de la programmation objet devient pesante. Ajouter des fonctions pour prendre en charge les opérations les plus courantes peut rendre les choses plus fluides. De nombreuses personnes définissent, par exemple, des méthodes inherit et method sur les objets.

 
Sélectionnez
Object.prototype.inherit = function(constructeurDeBase) {
  this.prototype = clone(constructeurDeBase.prototype);
  this.prototype.constructor = this;
};
Object.prototype.method = function(nom, func) {
  this.prototype[nom] = func;
};
 
function TableauEtrange(){}
TableauEtrange.inherit(Array);
TableauEtrange.method("push", function(valeur) {
  Array.prototype.push.call(this, valeur);
  Array.prototype.push.call(this, valeur);
});
 
var etrange = new TableauEtrange();
etrange.push(4);
show(etrange);

Si vous cherchez sur internet les mots « JavaScript » et « héritage », vous trouverez de nombreuses variantes de ces fonctions, certaines sont plus complexes et plus subtiles que celles ci-dessus.

Remarquez comment la méthode push écrite ici utilise la méthode push du prototype de son type parent. C'est quelque chose qui se fait fréquemment lors de l'utilisation de l'héritage - : une méthode du sous-type utilise en interne une méthode du super-type, mais en la modifiant d'une manière ou d'une autre.

La plus grande difficulté dans cette approche simpliste est la dualité entre les constructeurs et les prototypes. Les constructeurs ont un rôle vraiment central, ils sont le moyen par lequel les objets prennent leur nom, et quand vous avez besoin d'accéder à un prototype, vous devez passer par le constructeur et sa propriété prototype.

Cela ajoute beaucoup de frappes au clavier ("prototype" prend neuf lettres), de plus, c'est déroutant. Nous avons eu besoin d'écrire un constructeur vide et inutile pour TableauEtrange dans l'exemple précédent. Quelquefois, il m'est arrivé d'ajouter par erreur des méthodes à un constructeur au lieu de son prototype, ou d'essayer d'appeler Array.slice alors que je voulais appeler Array.prototype.slice. Autant que je sache, le prototype lui-même est l'aspect le plus important d'un type d'objet, et le constructeur n'est qu'une extension de cela, une méthode spéciale.

En ajoutant quelques méthodes simples d'aide à Object.prototype, il devient possible de créer une approche alternative aux objets et à l'héritage. Dans cette approche, un type est représenté par son prototype, et nous allons utiliser des variables en majuscules pour stocker ces prototypes. Quand il faut faire un peu de travail de « construction », cela est réalisé par une méthode appelée construct. Nous ajoutons une méthode appelée create au prototype Object, qui est utilisée à la place du mot-clé new. Elle clone l'objet, et appelle sa méthode construct, si une telle méthode existe, en lui passant en argument ceux qui ont été passés à create.

 
Sélectionnez
Object.prototype.create = function() {
  var objet = clone(this);
  if (typeof objet.construct == "function")
    objet.construct.apply(objet, arguments);
  return objet;
};

L'héritage peut être réalisé en clonant un objet prototype et en ajoutant ou remplaçant certaines de ses propriétés. Nous fournissons également une aide pratique pour réaliser cela, une méthode extend, qui clone l'objet sur lequel on l'appelle et qui ajoute à ce clone les propriétés de l'objet qui lui est donné en argument.

 
Sélectionnez
Object.prototype.extend = function(properties) {
  var resultat = clone(this);
  forEachIn(properties, function(nom, valeur) {
    resultat[nom] = valeur;
  });
  return resultat;
};

Dans le cas où il n'est pas prudent de tripoter le prototype Object, cela peut bien évidemment être implémenté avec des fonctions classiques (pas des méthodes).

Voici un exemple, si vous êtes suffisamment vieux, vous avez peut-être déjà joué à un jeu d'aventure en mode texte, où vous vous déplaciez dans un monde virtuel en tapant au clavier des commandes, et obteniez des réponses sous forme de texte décrivant ce qu'il y avait autour de vous et les actions que vous effectuiez. Ces jeux ont eu leur temps.

Nous pouvons écrire un prototype pour un élément d'un jeu de ce type.

 
Sélectionnez
var Produit = {
  construct: function(nom) {
    this.nom = nom;
  },
  examiner: function() {
    print("C'est ", this.nom, ".");
  },
  frapper: function() {
    print("Blang !");
  },
  prendre: function() {
    print("Vous ne pouvez pas soulever ", this.nom, ".");
  }
};
 
var lanterne = Produit.create("La lanterne en laiton");
lanterne.frapper();

Héritons de ce type de cette façon...

 
Sélectionnez
var ProduitDetaille = Produit.extend({
  construct: function(nom, details) {
    Produit.construct.call(this, nom);
    this.details = details;
  },
  examiner: function() {
    print("vous voyez ", this.nom, ", ", this.details, ".");
  }
});
 
var paresseuxGeant = ProduitDetaille.create(
  "le paresseux géant",
  "il s'accroche tranquillement sur un arbre en grignotant des feuilles");
paresseuxGeant.examiner();

Laisser de côté la partie obligatoire du prototype rend légèrement plus simples les choses comme l'appel à Produit.construct depuis le constructeur de ProduitDetaille. Remarquez que ce serait une mauvaise idée d'écrire simplement this.nom = nom dans ProduitDetaille.construct. Cela duplique une ligne. Bien sûr, dupliquer cette ligne est plus court qu'appeler la fonction Produit.construct mais si on se retrouve à ajouter plus tard quelque chose dans le constructeur, nous devrons l'ajouter à deux endroits différents.

La plupart du temps, le constructeur d'un sous-type commencera par appeler le constructeur du super-type. De cette façon, il démarre avec un objet valide du super-type, qu'il peut alors étendre. Dans cette nouvelle approche des prototypes, les types qui n'ont pas besoin de constructeur peuvent les laisser tomber. Ils hériteront automatiquement du constructeur de leur super-type.

var PetitProduit = Produit.extend({

 
Sélectionnez
  frapper: function() {
    print(this.nom, " vole à travers la pièce.");
  },
  prendre: function() {
    // (imaginez ici du code qui déplace l'objet dans votre poche)
    print("vous prenez ", this.nom, ".");
  }
});
 
var stylo = PetitProduit.create("le stylo rouge");
stylo.prendre();

Même si PetitProduit ne définit pas son propre constructeur, le créer avec un argument nom fonctionne, car il hérite du constructeur du prototype Produit.

JavaScript possède un opérateur appelé instanceof, qui peut être utilisé pour déterminer si un objet est basé sur un certain prototype. Vous lui donnez l'objet du côté gauche, et le constructeur du côté droit, et il renvoie un booléen, true si la propriété prototype du constructeur est le prototype direct ou indirect de l'objet, et false sinon.

Lorsque vous utilisez des constructeurs normaux, utiliser cet opérateur devient plutôt maladroit : il attend la fonction constructeur comme deuxième argument, mais nous avons seulement des prototypes. Une astuce similaire à la fonction clone peut être utilisée pour éviter cela . Nous utilisons un « faux constructeur », et nous lui appliquons instanceof.

 
Sélectionnez
Object.prototype.hasPrototype = function(prototype) {
  function FauxConstructeur() {}
  FauxConstructeur.prototype = prototype;
  return this instanceof FauxConstructeur;
};
 
show(stylo.hasPrototype(Produit));
show(stylo.hasPrototype(ProduitDetaille));

Ensuite, nous voulons créer un petit élément qui possède une description détaillée. Il semblerait que cet élément devrait hériter à la fois de ProduitDetaille et PetitProduit. JavaScript ne permet pas à un objet d'avoir plusieurs prototypes, et même s'il le permettait, le problème ne serait pas simple à résoudre. Par exemple, si PetitProduit voulait, pour une raison quelconque, définir aussi une méthode examiner, quelle méthode examiner ce nouveau prototype devrait-il utiliser ?

Dériver un type d'objet de plus d'un type parent est appelé héritage multiple. Certains langages se dégonflent et l'interdisent totalement, d'autres définissent des systèmes compliqués pour le faire marcher d'une manière pratique et bien définie. Il est possible d'implémenter un framework de multihéritage décent en JavaScript. En fait, il y a, comme d'habitude, de nombreuses bonnes façons pour réaliser cela. Mais elles sont toutes trop compliquées pour en discuter ici. À la place, je vais vous montrer une approche très simple qui est suffisante dans la plupart des cas.

Un mix-in est un type spécifique de prototype qui peut être « incorporé » à l'intérieur d'autres prototypes. PetitProduit peut être considéré comme un de ces prototypes. En copiant ses méthodes frapper et prendre dans un autre prototype, nous allons incorporer la petitesse dans ce prototype.

 
Sélectionnez
function mixInto(object, mixIn) {
  forEachIn(mixIn, function(nom, valeur) {
    object[nom] = valeur;
  });
};
 
var PetitProduitDetaille = clone(ProduitDetaille);
mixInto(PetitProduitDetaille, PetitProduit);
 
var sourisMorte = PetitProduitDetaille.create(
  "Fred la souris",
  "il est mort");
sourisMorte.examiner();
sourisMorte.frapper();

Rappelez-vous que forEachIn parcourt uniquement les propres propriétés de l'objet, il copiera donc frapper et prendre, mais pas le constructeur que PetitProduit a hérité de Produit.

Mélanger les prototypes devient plus complexe quand le mix-in a un constructeur, ou quand certaines de ses méthodes entrent en « collision » avec les méthodes du prototype dans lequel il est incorporé. Parfois, il est possible de faire un mix-in « manuellement ». Disons que nous avons un prototype Monstre, qui a son propre constructeur, et nous voulons le mélanger avec ProduitDetaille.

 
Sélectionnez
var Monstre = Produit.extend({
  construct: function(nom, estDangereux) {
    Produit.construct.call(this, nom);
    this.estDangereux = estDangereux;
  },
  frapper: function() {
    if (this.estDangereux)
      print(this.nom, " arrache votre tête avec ses dents.");
    else
      print(this.nom, " fuit en pleurant.");
  }
});
 
var MonstreDetaille = ProduitDetaille.extend({
  construct: function(nom, description, estDangereux) {
    ProduitDetaille.construct.call(this, nom, description);
    Monstre.construct.call(this, nom, estDangereux);
  },
  frapper: Monstre.frapper
});
 
var paresseuxGeant = MonstreDetaille.create(
  "le paresseux géant",
  "il s'accroche tranquillement sur un arbre en grignotant des feuilles",
  true);
paresseuxGeant.frapper();

Mais remarquez que cela conduit à appeler deux fois le constructeur de Produit lorsqu'on crée un MonstreDetaille : une fois à travers le constructeur de ProduitDetaille, et une fois à travers le constructeur de Monstre. Dans ce cas, il n'y a pas trop de dégâts, mais il existe des situations dans lesquelles cela pourrait poser problème.

Mais ne laissez pas ces complications vous décourager d'utiliser l'héritage. Les héritages multiples, même s'ils sont très utiles dans certaines situations, peuvent être ignorés sans problème la plupart du temps. C'est la raison pour laquelle certains langages comme Java s'en sortent en interdisant les héritages multiples. Et si, à un moment, vous pensez que vous en avez vraiment besoin, vous pouvez chercher sur Internet, faire quelques recherches, et trouver une approche qui fonctionne dans votre situation.

Maintenant que j'y pense, JavaScript serait probablement un fabuleux environnement de développement pour les aventures en mode texte. Cette capacité à modifier le comportement des objets à volonté, qui est ce que nous offre l'héritage par prototype, est très bien adaptée à cela. Si vous avez un objet herisson, qui a la capacité unique de rouler quand on lui tape dedans, vous pouvez simplement changer sa méthode frapper.

Malheureusement, les aventures en mode texte ont suivi le même chemin que les disques vinyles, alors qu'ils étaient populaires à une époque, ils ne sont joués de nos jours que par une petite population d'enthousiastes.


précédentsommairesuivant
Ces types sont souvent appelés des « classes » dans d'autres langages de programmation.
Pour rendre les choses plus simples, les créatures de notre terrarium se reproduiront de façon assexuée, d'elles-mêmes.
La paresse, pour un programmeur, n'est pas forcement un péché. Les personnes qui, laborieusement, font et refont toujours les mêmes choses tendent à être de bons travailleurs à la chaine et de mauvais programmeurs.

Licence Creative Commons
Le contenu de cet article est rédigé par Marijn Haverbeke et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2013 Developpez.com.