L'un des modèles de conception le plus déroutant est la persistance. La nécessité pour une application de conserver son état interne et ses données est telle qu'il existe probablement des dizaines, voire des centaines, de technologies différentes pour résoudre ce problème unique. Malheureusement, aucune technologie n'est une solution miracle. Chaque application, et parfois chaque composant de l’application, est unique à sa manière - ce qui nécessite une solution unique.
Dans ce tutoriel, je vais vous enseigner certaines des meilleures pratiques pour vous aider à déterminer quelle approche adopter lorsque vous travaillez sur de futures applications. J'aborderai brièvement quelques préoccupations et principes de conception de haut niveau, puis une vue plus détaillée du modèle de conception Active Record, combinée à quelques mots sur le modèle de conception Table Data Gateway..
Bien sûr, je ne vous enseignerai pas simplement la théorie derrière la conception, mais je vous guiderai également à travers un exemple qui commence par un code aléatoire et se transforme en une solution de persistance structurée..
Aujourd'hui, aucun programmeur ne peut comprendre ce système archaïque.
Le projet le plus ancien sur lequel je dois travailler a débuté en 2000. À l'époque, une équipe de programmeurs a démarré un nouveau projet en évaluant différentes exigences, en réfléchissant aux charges de travail que l'application va devoir gérer, en testant différentes technologies et en concluant ainsi: le code PHP de l'application, sauf le index.php
fichier, doit résider dans une base de données MySQL. Leur décision peut paraître scandaleuse aujourd’hui, mais elle était acceptable il y a douze ans (OK… peut-être pas).
Ils ont commencé par créer leurs tables de base, puis d'autres tables pour chaque page Web. La solution a fonctionné… pendant un temps. Les auteurs originaux savaient comment le conserver, mais chaque auteur s'en allait un par un, laissant la base de code entre les mains d'autres nouveaux venus..
Aujourd'hui, aucun programmeur ne peut comprendre ce système archaïque. Tout commence par une requête MySQL de index.php
. Le résultat de cette requête renvoie du code PHP qui exécute encore plus de requêtes. Le scénario le plus simple implique au moins cinq tables de base de données. Naturellement, il n'y a pas de tests ou de spécifications. La modification de quelque chose est une option, et nous devons simplement réécrire le module entier si quelque chose ne va pas.
Les développeurs d'origine ont ignoré le fait qu'une base de données ne devrait contenir que des données, pas de logique métier ni de présentation. Ils ont mélangé le code PHP et HTML avec MySQL et ont ignoré les concepts de conception de haut niveau.
Toutes les applications doivent se concentrer sur le respect d'une conception propre et de haut niveau.
Au fil du temps, les nouveaux programmeurs ont dû ajouter des fonctionnalités supplémentaires au système tout en corrigeant les anciens bugs. Il n'y avait aucun moyen de continuer à utiliser les tables MySQL pour tout, et toutes les personnes impliquées dans la maintenance du code ont convenu que sa conception était horriblement imparfaite. Ainsi, les nouveaux programmeurs ont évalué différentes exigences, ont réfléchi aux charges de travail que l’application devra gérer, ont testé différentes technologies et sont parvenus à une conclusion: ils ont décidé de transférer autant de code que possible dans la présentation finale. Encore une fois, cette décision peut paraître scandaleuse aujourd’hui, mais c’était à des années-lumière du précédent design scandaleux..
Les développeurs ont adopté un framework de templates et ont basé l'application autour de celui-ci, en commençant chaque nouvelle fonctionnalité et module par un nouveau modèle. C'était facile; le modèle était descriptif et ils savaient où trouver le code qui exécute une tâche spécifique. Mais c'est comme ça qu'ils ont fini avec des fichiers modèles contenant le langage DSL (Domain Specific Language) du moteur, HTML, PHP et bien sûr les requêtes MySQL.
Aujourd'hui, mon équipe regarde et fait des merveilles. C’est un miracle que de nombreuses vues fonctionnent réellement. Il faut parfois beaucoup de temps pour déterminer comment l’information passe de la base de données à la vue. Comme son prédécesseur, c'est tout un gâchis!
Ces développeurs ont ignoré le fait qu'une vue ne devrait pas contenir de logique métier ou de logique de persistance. Ils ont mélangé le code PHP et HTML avec MySQL et ont ignoré les concepts de conception de haut niveau.
Un mock est un objet qui agit comme son homologue réel, mais n'exécute pas le code réel..
Toutes les applications doivent se concentrer sur le respect d’une conception propre et de haut niveau. Ce n'est pas toujours réalisable, mais cela devrait être une haute priorité. Une bonne conception de haut niveau repose sur une logique métier bien isolée. La création, la persistance et la livraison des objets ne font pas partie du noyau et les dépendances ne pointent que vers la logique métier.
Isoler la logique métier ouvre la porte à de grandes possibilités et tout devient un peu un plugin, si les dépendances externes pointent toujours vers la logique métier. Par exemple, vous pouvez échanger la lourde base de données MySQL avec une base de données légère SQLite3..
Pour mieux identifier les problèmes liés à une conception médiocre, même si elle fonctionne, je vais commencer par un exemple simple de blog, vous l’avez deviné. Tout au long de ce didacticiel, je suivrai certains principes de développement piloté par les tests (TDD) et les rendrai facilement compréhensibles, même si vous n’avez pas d’expérience TDD. Imaginons que vous utilisiez un framework MVC. Lors de la sauvegarde d’un article de blog, un contrôleur nommé BlogPost
exécute un enregistrer()
méthode. Cette méthode se connecte à une base de données SQLite pour stocker une publication de blog dans la base de données..
Créons un dossier, appelé Les données dans le dossier de notre code et accédez à ce répertoire dans la console. Créez une base de données et une table, comme ceci:
$ sqlite3 MyBlog SQLite version 3.7.13 2012-06-11 02:05:22 Entrez ".help" pour obtenir des instructions. Entrez les instructions SQL terminées par un ";" sqlite> create table BlogPosts (title varchar (120), clé primaire, texte du contenu, horodatage publié_horodatage);
Notre enregistrer()
méthode obtient les valeurs du formulaire sous forme de tableau, appelé $ data
:
classe BlogPostController function save ($ data) $ dbhandle = new SQLite3 ('Data / MyBlog'); $ query = 'INSERER DANS LES VALEURS BlogPosts ("'. $ data ['title']. ',"'. $ data ['content']. '","'. time (). "") '; $ dbhandle-> exec ($ query);
Ce code fonctionne, et vous pouvez le vérifier en l’appelant d’une autre classe, en passant un paramètre prédéfini. $ data
tableau, comme ceci:
$ this-> object = new BlogPostController; $ data ['title'] = 'Titre du premier message'; $ data ['content'] = 'Du contenu sympa pour le premier message'; $ data ['published_timestamp'] = time (); $ this-> object-> save ($ data);
Le contenu de la $ data
La variable a bien été enregistrée dans la base de données:
sqlite> select * from BlogPosts; Titre du premier message | Du contenu intéressant pour le premier message | 1345665216
L'héritage est le type de dépendance le plus puissant.
Un test de caractérisation décrit et vérifie le comportement actuel du code préexistant. Il est le plus souvent utilisé pour caractériser le code hérité et facilite grandement sa refactorisation..
Un test de caractérisation peut tester un module, une unité ou aller de l'interface utilisateur à la base de données. Tout dépend de ce que nous voulons tester. Dans notre cas, un tel test devrait exercer le contrôleur et vérifier le contenu de la base de données. Ce n'est pas un test unitaire, fonctionnel ou d'intégration typique, et il ne peut généralement pas être associé à l'un ou l'autre de ces niveaux de test..
Les tests de caractérisation constituent un filet de sécurité temporaire. Nous les supprimons généralement une fois le code refactorisé et testé. Voici une implémentation d’un test, placé dans le Tester dossier:
require_once '… /BlogPostController.php'; La classe BlogPostControllerTest étend PHPUnit_Framework_TestCase private $ object; private $ dbhandle; function setUp () $ this-> object = new BlogPostController; $ this-> dbhandle = new SQLite3 ('… / Data / MyBlog'); function testSave () $ this-> cleanUPDatabase (); $ data ['title'] = 'Titre du premier message'; $ data ['content'] = 'Du contenu sympa pour le premier message'; $ data ['published_timestamp'] = time (); $ this-> object-> save ($ data); $ this-> assertEquals ($ data, $ this-> getPostsFromDB ()); fonction privée cleanUPDatabase () $ this-> dbhandle-> exec ('DELETE FROM BlogPosts'); fonction privée getPostsFromDB () $ result = $ this-> dbhandle-> query ('SELECT * FROM BlogPosts'); return $ result-> fetchArray (SQLITE3_ASSOC);
Ce test crée un nouvel objet contrôleur et exécute son enregistrer()
méthode. Le test lit ensuite les informations de la base de données et les compare aux données prédéfinies. $ data []
tableau. Nous effectuons cette comparaison en utilisant le $ this-> assertEquals ()
méthode, une assertion qui suppose que ses paramètres sont égaux. S'ils sont différents, le test échoue. En outre, nous nettoyons le BlogPosts
table de base de données chaque fois que nous exécutons le test.
Le code hérité est un code non testé. - Michael Feathers
Avec notre test opérationnel, nettoyons un peu le code. Ouvrez la base de données avec le nom de répertoire complet et utilisez sprintf ()
pour composer la chaîne de requête. Cela se traduit par un code beaucoup plus simple:
classe BlogPostController function save ($ data) $ dbhandle = new SQLite3 (__ DIR__. '/ Data / MyBlog'); $ query = sprintf ('INSERER DANS LES VALEURS BlogPosts ("% s", "% s", "% s") ", $ data [" titre "], $ data [" contenu "], time ()); $ dbhandle-> exec ($ query);
Nous reconnaissons que notre code doit être déplacé du contrôleur vers la couche de logique métier et de persistance, et le modèle de passerelle peut nous aider à démarrer dans cette voie. Voici la version révisée testSave ()
méthode:
function testItCanPersistABlogPost () $ data = array ('title' => 'Titre du premier message', 'content' => 'Du contenu.', 'timestamp' => time ()); $ blogPost = new BlogPost ($ data ['titre'], $ data ['contenu'], $ data ['horodatage']); $ mockedPersistence = $ this-> getMock ('SqlitePost'); $ mockedPersistence-> attend ($ this-> once ()) -> méthode ('persist') -> avec ($ blogPost); $ controller = new BlogPostController ($ mockedPersistence); $ controller-> save ($ data);
Ceci représente comment nous voulons utiliser le enregistrer()
méthode sur le contrôleur. Nous nous attendons à ce que le contrôleur appelle une méthode nommée persist ($ blogPostObject)
sur l'objet passerelle. Changeons notre BlogPostController
pour faire ça:
classe BlogPostController private $ gateway; function __construct (Gateway $ gateway = null) $ this-> gateway = $ gateway? : new SqlitePost (); function save ($ data) $ this-> gateway-> persist (nouveau BlogPost ($ data ['titre'], $ data ['contenu'], $ data ['horodatage']));
Une bonne conception de haut niveau avec une logique métier bien isolée.
Agréable! Notre BlogPostController
est devenu beaucoup plus simple. Il utilise la passerelle (fournie ou instanciée) pour conserver les données en appelant son persister()
méthode. Il n'y a absolument aucune connaissance sur la façon dont les données sont conservées; la logique de persistance est devenue modulaire.
Lors du test précédent, nous avons créé le contrôleur avec un moquer objet de persistance, en veillant à ce que les données ne soient jamais écrites dans la base de données lors de l'exécution du test. Dans le code de production, le contrôleur crée son propre objet persistant pour conserver les données à l'aide d'un SqlitePost
objet. Un mock est un objet qui agit comme son homologue réel, mais n'exécute pas le code réel..
Récupérons maintenant un article de blog du magasin de données. C’est aussi simple que de sauvegarder des données, mais notez que j’ai un peu remodelé le test.
require_once '… /BlogPostController.php'; require_once '… /BlogPost.php'; require_once '… /SqlitePost.php'; La classe BlogPostControllerTest étend PHPUnit_Framework_TestCase private $ mockedPersistence; contrôleur privé $; données privées $; function setUp () $ this-> mockedPersistence = $ this-> getMock ('SqlitePost'); $ this-> controller = new BlogPostController ($ this-> mockedPersistence); $ this-> data = array ('title' => 'Titre du premier message', 'content' => 'Du contenu.', 'Horodatage' => time ()); function testItCanPersistABlogPost () $ blogPost = $ this-> aBlogPost (); $ this-> mockedPersistence-> attend ($ this-> once ()) -> méthode ('persist') -> avec ($ blogPost); $ this-> controller-> save ($ this-> data); function testItCanRetrievABlogPostByTitle () $ attenduBlogpost = $ this-> aBlogPost (); $ this-> mockedPersistence-> attend ($ this-> once ()) -> méthode ('findByTitle') -> avec ($ this-> data ['title']) -> will ($ this-> returnValue ( $ attendueBlogpost)); $ this-> assertEquals ($ attenduBlogpost, $ this-> contrôleur-> findByTitle ($ this-> data ['title'])); fonction publique aBlogPost () retourne le nouveau BlogPost ($ this-> data ['title'], $ this-> data ['content'], $ this-> data ['timestamp']);
Et la mise en œuvre dans le BlogPostController
est juste une méthode à une déclaration:
fonction findByTitle ($ title) return $ this-> passerelle-> findByTitle ($ title);
N'est-ce pas cool? le BlogPost
class fait maintenant partie de la logique métier (rappelez-vous du schéma de conception de haut niveau présenté ci-dessus) L’UI / MVC crée BlogPost
objets et utilise du béton passerelle
implémentations pour conserver les données. Toutes les dépendances pointent vers la logique métier.
Il ne reste plus qu’une étape: créer une implémentation concrète de passerelle
. Voici le SqlitePost
classe:
require_once 'Gateway.php'; la classe SqlitePost implémente Gateway private $ dbhandle; function __construct ($ dbhandle = null) $ this-> dbhandle = $ dbhandle? : new SQLite3 (__DIR__. '/ Data / MyBlog'); fonction publique persistante (BlogPost $ blogPost) $ query = sprintf ('INSERER DANS LES VALEURS BlogPosts ("% s", "% s", "% s") ", $ blogPost-> titre, $ blogPost-> contenu, $ blogPost-> timestamp); $ this-> dbhandle-> exec ($ requête); fonction publique findByTitle ($ title) $ SqliteResult = $ this-> dbhandle-> query (sprintf ('SELECT * DE BlogPosts WHERE title = "% s"', $ title)); $ blogPostAsString = $ SqliteResult-> fetchArray (SQLITE3_ASSOC); retourne le nouveau BlogPost ($ blogPostAsString ['titre'], $ blogPostAsString ['contenu'], $ blogPostAsString ['horodatage']);
Remarque: le test de cette implémentation est également disponible dans le code source, mais, en raison de sa complexité et de sa longueur, je ne l'ai pas inclus ici..
Active Record est l'un des modèles les plus controversés. Certains l'acceptent (comme Rails et CakePHP), d'autres l'évitent. Beaucoup Cartographie relationnelle d'objet Les applications (ORM) utilisent ce modèle pour enregistrer des objets dans des tableaux. Voici son schéma:
Comme vous pouvez le constater, les objets basés sur un enregistrement actif peuvent persister et se récupérer. Ceci est généralement réalisé en prolongeant un ActiveRecordBase
classe, une classe qui sait comment travailler avec la base de données.
Le plus gros problème avec Active Record est le s'étend dépendance. Comme nous le savons tous, l'héritage est le type de dépendance le plus puissant et il est préférable de l'éviter la plupart du temps..
Avant d'aller plus loin, voici où nous en sommes maintenant:
L'interface de passerelle appartient à la logique métier et ses implémentations concrètes à la couche de persistance. Notre BlogPostController
a deux dépendances, toutes deux pointant vers la logique métier: le SqlitePost
passerelle et BlogPost
classe.
Il y a beaucoup d'autres modèles, comme le Modèle de proxy, qui sont étroitement liés à la persistance.
Si nous devions suivre le modèle Active Record exactement tel qu'il est présenté par Martin Fowler dans son ouvrage de 2003 intitulé Patterns of Enterprise Application Architecture, nous aurions besoin de déplacer les requêtes SQL dans le BlogPost
classe. Cela pose cependant le problème de violer à la fois le Principe d'inversion de dépendance et le Principe Ouvert Fermé. Le principe d'inversion de dépendance stipule que:
Et le principe Ouvert Fermé dit: les entités logicielles (classes, modules, fonctions, etc.) doivent être ouvertes pour extension, mais fermées pour modification. Nous adopterons une approche plus intéressante et intégrerons la passerelle dans notre solution Active Record..
Si vous essayez de le faire vous-même, vous avez probablement déjà compris que l'ajout du modèle d'enregistrement actif au code nuirait à la tâche. Pour cette raison, j’ai pris l’option de désactiver le contrôleur et de SqlitePost
tests pour se concentrer uniquement sur BlogPost
classe. Les premières étapes sont: faire BlogPost
se charger en définissant son constructeur comme privé et le connecter à l'interface de passerelle. Voici la première version du BlogPostTest
fichier:
require_once '… /BlogPost.php'; require_once '… /InMemoryPost.php'; require_once '… /ActiveRecordBase.php'; La classe BlogPostTest étend PHPUnit_Framework_TestCase function testItCanConnectPostToGateway () $ blogPost = BlogPost = load (); $ blogPost-> setGateway ($ this-> inMemoryPost ()); $ this-> assertEquals ($ blogPost-> getGateway (), $ this-> inMemoryPost ()); function testItCanCreateANewAndEmptyBlogPost () $ blogPost = BlogPost :: load (); $ this-> assertNull ($ blogPost-> title); $ this-> assertNull ($ blogPost-> content); $ this-> assertNull ($ blogPost-> timestamp); $ this-> assertInstanceOf ('Gateway', $ blogPost-> getGateway ()); fonction privée inMemoryPost () retour new InMemoryPost ();
Il teste qu'un article de blog est correctement initialisé et qu'il peut avoir une passerelle s'il est défini. Il est recommandé d’utiliser plusieurs assertions lorsque tous testent le même concept et la même logique..
Notre deuxième test a plusieurs assertions, mais toutes font référence au même concept commun de article de blog vide. Bien sûr, le BlogPost
La classe a également été modifiée:
classe BlogPost private $ title; contenu privé $; horodatage privé $; passerelle statique privée; fonction privée __construct ($ title = null, $ content = null, $ timestamp = null) $ this-> title = $ title; $ this-> content = $ content; $ this-> timestamp = $ timestamp; function __get ($ name) return $ this -> $ name; function setGateway ($ gateway) self :: $ gateway = $ gateway; function getGateway () return self :: $ gateway; fonction statique load () if (! self :: $ gateway) self :: $ gateway = new SqlitePost (); retourner le nouveau soi;
Il a maintenant un charge()
méthode qui retourne un nouvel objet avec une passerelle valide. À partir de ce moment, nous poursuivrons la mise en œuvre d’un charger ($ title)
méthode pour créer un nouveau BlogPost
avec des informations de la base de données. Pour faciliter les tests, j’ai mis en place un InMemoryPost
classe pour la persistance. Il conserve simplement une liste d'objets en mémoire et renvoie les informations souhaitées:
La classe InMemoryPost implémente Gateway private $ blogPosts = array (); fonction publique findByTitle ($ blogPostTitle) return array ('title' => $ this-> blogPosts [$ blogPostTitle] -> title, 'content' => $ this-> blogPosts [$ blogPostTitle] -> content, 'timestamp' => $ this-> blogPosts [$ blogPostTitle] -> timestamp); fonction publique persistante (BlogPost $ blogPostObject) $ this-> blogPosts [$ blogPostObject-> title] = $ blogPostObject;
Ensuite, je me suis rendu compte que l’idée initiale de connecter le BlogPost
à une passerelle via une méthode séparée était inutile. J'ai donc modifié les tests en conséquence:
La classe BlogPostTest étend PHPUnit_Framework_TestCase function testItCanCreateANewAndEmptyBlogPost () $ blogPost = BlogPost :: load (); $ this-> assertNull ($ blogPost-> title); $ this-> assertNull ($ blogPost-> content); $ this-> assertNull ($ blogPost-> timestamp); function testItCanLoadABlogPostByTitle () $ gateway = $ this-> inMemoryPost (); $ aBlogPosWithData = $ this-> aBlogPostWithData ($ gateway); $ gateway-> persist ($ aBlogPosWithData); $ this-> assertEquals ($ aBlogPosWithData, BlogPost :: load ('some_title', null, null, $ passerelle)); fonction privée inMemoryPost () retour new InMemoryPost (); fonction privée aBlogPostWithData ($ gateway = null) return BlogPost :: load ('some_title', 'some content', '123', $ gateway);
Comme vous pouvez le constater, j’ai radicalement changé de chemin BlogPost
est utilisé.
classe BlogPost private $ title; contenu privé $; horodatage privé $; fonction privée __construct ($ title = null, $ content = null, $ timestamp = null) $ this-> title = $ title; $ this-> content = $ content; $ this-> timestamp = $ timestamp; function __get ($ name) return $ this -> $ name; charge de la fonction statique ($ title = null, $ content = null, $ timestamp = null, $ gateway = null) $ gateway = $ gateway? : new SqlitePost (); if (! $ content) $ postArray = $ gateway-> findByTitle ($ title); if ($ postArray) renvoie un nouveau self ($ postArray ['titre'], $ postArray ['contenu'], $ postArray ['horodatage']); retourne un nouveau self ($ title, $ content, $ timestamp);
le charge()
méthode vérifie la $ contenu
paramètre pour une valeur et crée un nouveau BlogPost
si une valeur a été fournie. Sinon, la méthode essaie de trouver un article de blog avec le titre donné. Si un message est trouvé, il est renvoyé; s'il n'y en a pas, la méthode crée un vide BlogPost
objet.
Pour que ce code fonctionne, nous devrons également modifier le fonctionnement de la passerelle. Notre implémentation doit renvoyer un tableau associatif avec Titre
, contenu
, et horodatage
éléments au lieu de l'objet lui-même. C'est une convention que j'ai choisie. Vous pouvez trouver d'autres variantes, comme un tableau simple, plus attrayantes. Voici les modifications dans SqlitePostTest
:
function testItCanRetrieveABlogPostByItsTitle () […] // nous attendons un tableau au lieu d'un objet $ this-> assertEquals ($ this-> blogPostAsArray, $ gateway-> findByTitle ($ this-> blogPostAsArray ['titre']); fonction privée aBlogPostWithValues () // nous utilisons une charge statique à la place de l'appel constructeur. blogPost = BlogPost = load ($ this-> blogPostAsArray ['title'], $ this-> blogPostAsArray ['contenu'], $ this -> blogPostAsArray ['timestamp']);
Et les changements d'implémentation sont:
fonction publique findByTitle ($ title) $ SqliteResult = $ this-> dbhandle-> query (sprintf ('SELECT * FROM BlogPosts WHERE titre = "% s"', $ title)); // retourne le résultat directement, ne construit pas l'objet return $ SqliteResult-> fetchArray (SQLITE3_ASSOC);
On a presque fini. Ajouter un persister()
méthode à la BlogPost
et appelez toutes les méthodes nouvellement implémentées à partir du contrôleur. Voici la persister()
méthode qui utilisera simplement la passerelle persister()
méthode:
fonction privée persist () $ this-> gateway-> persist ($ this);
Et le contrôleur:
classe BlogPostController function save ($ data) $ blogPost = BlogPost :: load ($ data ['title'], $ data ['content'], $ data ['horodatage']); $ blogPost-> persist (); function findByTitle ($ title) return BlogPost :: load ($ title);
le BlogPostController
est devenu si simple que j'ai enlevé tous ses tests. Il appelle simplement le BlogPost
objets persister()
méthode. Naturellement, vous voudrez ajouter des tests si et quand vous avez plus de code dans le contrôleur. Le téléchargement de code contient toujours un fichier de test pour BlogPostController
, mais son contenu est commenté.
Ce n'est que la pointe de l'iceberg.
Vous avez vu deux implémentations de persistance différentes: le passerelle et Enregistrement actif modèles. À partir de ce moment, vous pouvez implémenter une ActiveRecordBase
classe abstraite à étendre pour toutes vos classes qui ont besoin de persistance. Cette classe abstraite peut utiliser différentes passerelles afin de conserver des données, et chaque implémentation peut même utiliser une logique différente pour répondre à vos besoins..
Mais ce n’est que la pointe de l’iceberg. Il existe de nombreux autres modèles, tels que le Modèle de proxy, qui sont étroitement liés à la persistance; chaque modèle fonctionne pour une situation particulière. Je vous recommande de toujours commencer par mettre en œuvre la solution la plus simple, puis de mettre en place un autre modèle lorsque vos besoins changent..
J'espère que vous avez apprécié ce tutoriel et que j'attends avec impatience vos opinions et vos implémentations alternatives à ma solution dans les commentaires ci-dessous..