Modèle objet de PHP, suite

Licence Informatique, semestre 6

Alexandre NiveauJean-Marc Lecarpentier — Ludovic Jean-Baptiste

Enseignement des technologies du Web

 

Modèle objet de PHP, suite

Notes de cours

Travail personnel

Objectifs

Se remettre en tête les concepts de base de la POO et les principes d'une bonne organisation du code, et manipuler les outils que propose PHP.

Exercice 1 — Le retour de l’automate cellulaire élémentaire #

On prend la suite de l'exercice du TP précédent. Attention : avant de commencer celui-ci, il est nécessaire d'avoir terminé la partie « basique » de l'exo précédent, c'est-à-dire jusqu'à la partie « Améliorations » non comprise.

Si vous avez déjà commencé des « Améliorations », il faudra peut-être adapter l'énoncé, je compte sur votre discernement !

Remarque : il est conseillé de mettre des types au paramètres et valeurs de retour des fonctions et méthodes, dans la mesure du possible. Il est également conseillé d'utiliser la vérification stricte des types. Voir cours.

Architecture pour les règles d'évolution

Comme expliqué en cours, la façon dont la règle 110 a été implémentée dans le programme (NB: avant la partie Améliorations) n'est pas très satisfaisante : il faut modifier le code de WorldState si on veut utiliser une autre règle. Pour éviter ça, une possibilité pourrait être de rendre WorldState abstraite et de lui ajouter une méthode abstraite computeNextStateCell, qui serait définie dans des sous-classes WorldStateRule110, WorldStateRule150, etc.

Cette solution est cependant un peu lourde, et limite la flexibilité du code. Une meilleure idée est d'utiliser la composition : WorldState va faire appel à un objet tiers pour le calcul de l'état suivant, et cet objet tiers pourra avoir diverses implémentations (un par règle). Ainsi la classe WorldState reste indépendante du changement de règle, et elle peut être elle-même modifiée sans risque d'impacter des sous-classes.

  1. Créer une interface EvolutionRule, qui doit définir une seule méthode computeNextStateCell($leftAlive, $selfAlive, $rightAlive).
  2. Créer une classe Rule110 qui implémente l'interface, et supprimer la fonction compute_next_state_rule110 faite au TP précédent, si elle existe encore.
  3. Modifier la méthode computeNextGeneration de WorldState : elle doit prendre en paramètre une instance de EvolutionRule, et utiliser sa méthode pour le calcul de la génération suivante. NB: vous devez forcer le paramètre à avoir le type EvolutionRule.
  4. L'instance de EvolutionRule va donc devoir être donnée par notre Simulator. A priori, c'est à la construction du Simulator que la règle va être choisie. Par conséquent, ajouter une propriété $evolutionRule à la classe Simulator, propriété qui doit être initialisée via un paramètre du constructeur. Modifier ensuite l'appel à computeNextState.
  5. Tester, en donnant au simulateur une instance de Rule110 : le programme doit fonctionner comme avant.

À présent, pour utiliser une autre règle, il suffit de créer une implémentation différente de EvolutionRule : implémenter une autre règle, par exemple la règle 184, et tester. Voir ici des dessins d'un bon nombre d'autres règles.

Remarque : si vous avez implémenté la section « D'autres règles » dans la partie « Améliorations » du TP précédent, vous devriez vous rendre compte que ce qu'on a fait ici est très similaire : dans les deux cas, on passe simplement une fonction à WorldState pour lui expliquer comment mettre à jour les cellules. La version objet est un peu plus lourde (la fonction est enveloppée dans un objet), mais a l'avantage de préciser des types très explicites, qui (1) peuvent éviter des erreurs et (2) améliorent l'organisation du code.

Architecture pour l'affichage

L'utilisation d'une méthode __toString pour la partie « affichage » du programme est pratique, mais pas très robuste : on est dépendant de l'implémentation de __toString (on ne peut pas décider de changer les caractères, ou la taille des cellules, depuis l'extérieur de la classe), et réciproquement, on ne peut pas faire évoluer __toString n'importe comment. Cette partie vise à vous montrer ça.

Modification du __toString, et impact sur le programme
  1. On va ajouter une propriété age à WorldState, qui sera initialisée à 0 dans le constructeur, et incrémentée dans computeNextGeneration comme il se doit. Elle aura bien sûr la visibilité privée, et on lui ajoutera un accesseur (mais pas de mutateur — on veut toujours que WorldState soit immutable).
  2. Modifier le __toString pour que l'âge du monde apparaisse entre parenthèses au début de la chaîne. Relancer le programme pour voir le résultat.

Notre nouvelle méthode __toString est pratique pour faire des tests sur le programme, mais elle impacte l'affichage des simulations : ce n'est pas (forcément) ce qu'on veut.

De manière générale il est une bonne idée de séparer le modèle, ou logique métier, du programme, et l'affichage, qui peut avoir besoin de varier en fonction des contextes. Nous allons nous y employer.

Séparation modèle et vue
  1. Créer une interface Displayer avec une seule méthode, displayWorld, qui doit prendre en paramètre une instance de WorldState.
  2. Modifier Simulator pour qu'il utilise une instance de Displayer pour afficher le monde, plutôt que de faire un echo.
  3. Créer TerminalDisplayer, l'implémentation de Displayer qui correspond à l'affichage qu'on avait auparavant (sans l'âge). NB: on peut passer en paramètre du constructeur de TerminalDisplayer les caractères à utiliser pour les cellules blanches et noires ! Ce n'était pas vraiment possible de le faire quand on utilisait le __toString de WorldState sans alourdir futilement l'API de la classe.
  4. Passer une instance de TerminalDisplayer au simulateur, et tester que le programme fonctionne bien comme avant.
Contrôle plus fin de la vitesse des itérations

Notre programme affiche l'évolution du monde, mais si on veut afficher un grand nombre d'itérations, le défilement peut être très rapide. On voudrait pouvoir le gérer plus finement : soit en faisant une petite pause après chaque affichage, soit en attendant que l'utilisateurtrice appuie sur Entrée avant de passer à la génération suivante.

  1. Ajouter une méthode abstraite iterationControl() à TerminalDisplayer (qui va donc devoir être une classe abstraite). Cette méthode doit être appelée à la fin de displayWorld.
  2. Créer une sous-classe InteractiveTerminalDisplayer qui implémente iterationControl en attendant l'appui sur Entrée (rappelez-vous de la fonction readline vue au premier TP), et tester le résultat.
  3. Créer une autre sous-classe, PausingTerminalDisplayer, qui implémente iterationControl en attendant un certain nombre de microsecondes (éventuellement paramétrable) (voir la fonction usleep — qui ne marche pas forcément sous windows). Tester le résultat.
  4. Optionnel : créer une classe AnimatedTerminalDisplayer qui étend PausingTerminalDisplayer. Elle doit appeler le iterationControl de sa parente, puis afficher la chaîne suivante : "\e[2J\e[1;1H". Il s'agit de séquences d'échappement qui effacent le contenu du terminal (pour les terminaux compatibles — ça devrait toujours marcher sous Linux, sans doute aussi sous OSX et BSD, et pour Windows ça dépend des versions). Ce displayer permet donc de voir l'évolution du monde de façon animée (ça marche mieux si les itérations ne sont pas trop rapides, typiquement 3-5 FPS). Tester le résultat.
Un affichage différent
  1. Créer une classe StatsDisplayer, qui implémente Displayer en montrant non pas l'état du monde, mais des statistiques dessus : âge et nombre de cellules noires/blanches. Tester.
  2. Comment faire pour avoir l'affichage des statistiques avec un défilement lent / interactif, comme on l'a fait dans la section précédente ? Les choix d'architecture pour le contrôle de la vitesse des itérations étaient-ils bons ? Comment aurait-on pu faire ?

Si vous avez fini…

Retournez voir la partie « Améliorations » de l'exo du TP précédent. NB : pour la toute dernière question, on pourra avoir envie d'implémenter un ImageDisplayer, mais cela pourra nécessiter de complexifier un peu le code du Simulator.