Test Driven Development (TDD) : Guide pour un Développement Efficace

Découvrez le Test Driven Development : avantages, meilleures pratiques et impact sur la qualité du code. Maîtrisez l'art du TDD aujourd'hui !

L'origine


Le Test-Driven Development plus communément appelé TDD n'a pas été inventé par une seule personne mais par plusieurs développeurs. Kent Beck est souvent la personne la plus affiliée au TDD parce qu'il est celui qui l'a popularisé lors de ses travaux sur la méthodologie Extreme Programming à la fin des années 90. Néanmoins il est important de rappeler qu'il n'est pas le créateur de l'outil qu'est le TDD, il a été mis en place pour tout un tas de personne, dont notamment Martin Fowler (co-auteur du Manifeste Agile) ou encore Ward Cunningham et Ron Jeffries qui ont fondé l'Extreme Programming avec Kent Beck.


Un outil de travail


L'outil


Le TDD est un outil de travail qui permet au développeur de coder en étant guidé par les tests. Il permet d'avancer étape par étape, petit pas par petit pas, pour avancer vers une solution toujours plus fiable.


Cette solution plus fiable est notamment due à sa forte proximité avec les principes SOLID. En effet, le TDD facilite la mise en place des principes SOLID. En écrivant d'abord des tests, il est plus naturel de concevoir un code simple et cohérent qui respecte les principes tels que l'encapsulation, le découplage, la gestion des dépendances et la séparation des responsabilités. On se retrouve plus naturellement avec des modules indépendants et l'injection de dépendances, ce qui conduit à du code plus propre.


Le TDD et les principes SOLID vont donc de pair pour une conception logicielle de qualité.



  1. Nous avons une première phase (rouge sur le schéma) où nous devons écrire un test qui échoue, basé sur une spécification. NB : on n'écrit pas de code de production (code final) tant que le test n'est pas en échec.
  2. La deuxième phase (verte sur le schéma) consiste à faire passer le test en succès en écrivant du code de production et tout en restant le plus simple possible. NB : on écrit uniquement le code nécessaire pour faire passer le test et sans retourner dans le code du test.
  3. La dernière phase (bleue sur le schéma) concerne le refactoring. On met à jour le code en appliquant les bonnes pratiques, le clean code, etc. Une fois que le refactoring est fait, si les tests passent toujours, on conserve le refactor et on passe aux prochaines spécifications, sinon, on annule le refactor et on essaye autre chose. NB : ne doit jamais effectuer de refactoring dans les autres phases, seulement ici.


L'approche TDD est un développement itératif et incremental qui met l'accent sur la qualité du code.


Le manifeste


Le TDD respecte certains principes qui sont :


  • Baby steps instead of large-scale changes

On avance petit pas par petit pas pour avoir une boucle de feedback rapide et régulière.


  • Continuous refactoring instead of late quality improvements

On améliore le code en continue, on s'en occupe tout de suite parce qu'un refactor annoncé pour plus tard n'arrive finalement en général jamais.


  • Evolutionary design instead of big design up front

On développe ce qui est nécessaire et suffisant et on évolue progressivement.


  • Executable documentation instead of static documents

Les tests mis en place pendant le TDD sont en réalité une documentation executable. L'idée est de lier la documentation avec le code pour s'assurer qu'elle est bien à jour et maintenue.


  • Minimalist code instead of gold-plated solution

Un code simple et qui fonctionne plutôt qu'une solution surdimensionnées avec un niveau de complexité bien trop élevé et pas nécessaire.




Le test propre

Given When Then


L'approche Given When Then est basée sur une convention développée dans le cadre du Behaviour-Driven Development autrement appelé BDD. Il s'agit d'une approche de développement axée sur la collaboration et la spécification du comportement à travers ds scénarios clairs et compréhensibles.


En utilisant cette convention on divise le test en trois parties :

  1. Given, la condition préalable au test
  2. When, l'exécution du système testé
  3. Then, le comportement attendu


Exemple : Given user is not logged in When user logs in Then user is logged in successfully


Should When


L'approche Should When est une convention de nommage facile à lire et plus largement utilisée.


En utilisant cette convention on divise le test en deux parties :

  1. When, la condition préalable au test
  2. Should, le comportement attendu


Exemple : Should have user logged in When user logs in


Arrange Act Assert


Le modèle Arrange Act Assert autrement appelé AAA est un pattern descriptif et révélateur des intentions pour structurer le test.


Le test est alors organisé de la manière suivante :

  1. La partie Arrange contient la logique de configuration du test. C'est ici qu'on initialise le test.
  2. La partie Act execute le système que l'on souhaite tester. C'est ici qu'on fait l'appel d'une fonction, d'un composant, d'un call API, etc.
  3. La partie Assert, vérifie que le système testé se comporte comme prévu. C'est ici qu'on vérifie que le résultat obtenu correspond au résultat attendu.


Exemple :




F.I.R.S.T.


Le principe F.I.R.S.T. est un ensemble de principes qui définissent les caractéristiques d'un test propre et de qualité.


Ces caractéristiques sont les suivantes :

  • Fast, un test doit être rapide et efficace de manière à pouvoir être exécuté fréquemment pour avoir un feedback régulier
  • Independent, les tests doivent être indépendants les uns des autres afin d'être exécutable individuellement et efficacement
  • Repeatable, un test doit être répétable dans n'importe quel environnement et à tout moment
  • Self-Validating, les tests doivent retourner un succès ou un échec afin de vérifier lui même si l'exécution du test a réussi ou échoué sans évaluation manuelle
  • Timely, les tests doivent être écrit avant ou en même temps que le code de production. ils doivent être maintenus et exécutés régulièrement


Quand l'utiliser ?


Quand ne pas l'utiliser plutôt !


Il n'est pas nécessaire et pas utile de faire du TDD quand on a pas de spécifications, quand les tests n'apportent rien, quand les tests sont trop lents, quand il n'y a pas de logique.


La démo


L'incontournable Fizz Buzz


Le Fizz Buzz est un exercice populaire qui permet d'appréhender la méthode TDD. Ce n'est qu'un échantillon et qu'un début de la méthode, mais ça reste intéressant.


Les spécifications sont les suivantes :

  • On commence à compter à partir de 1 jusqu'à 100
  • Lorsqu'on rencontre un nombre divisible par 3, on retourne "Fizz"
  • Lorsqu'on rencontre un nombre divisible par 5, on retourne "Buzz"
  • Lorsqu'on rencontre un nombre divisible par 3 et par 5, on retourne "Fizz-Buzz"


Nous allons utiliser le TypeScript pour mettre en place cet algorithme et le tester.


Première spécification


On créé un fichier test fizz-buzz.test.ts et on créé notre premier test qui va gérer la spécification où on teste le nombre 1.





Ici, le test ne passe pas parce qu'on a pas encore créé la fonction fizzBuzz() et encore moins l'algorithme associé. Nous sommes donc à la première phase du TDD, la phase rouge, celle où on écrit un test qui échoue.


On va maintenant passer à la deuxième phase du TDD, celle où on va faire passer le test au vert. Pour cela, on va créer le fichier fizz-buzz.ts et écrire le code permettant de gérer notre cas de spécification.




Ici, on pourrait avoir tendance à faire un return String(n) directement, mais TDD nous dit de commence par écrire le code le plus simple possible pour faire passer le test au vert, et en réalité le plus simple et rapide et de tout simplement retourner 1 directement.


On va passer au prochain test qui est de tester l'input 2.



Le test échoue parce qu'on a pas encore géré ce cas dans notre fonction. On retourne donc dans notre fonction et on essaye de résoudre ce cas de la manière la plus simple et rapide.




Ici, encore une fois, le plus rapide est de retourner 2 si on a 2 en input.


Maintenant on arrive à la troisième et dernière phase du TDD, celle du refactor. En effet, actuellement nos tests passent avec succès, le code est simple, mais il pourrait l'être encore plus en appliquant de bonnes pratiques.


On va donc revenir sur notre fonction fizzBuzz() sans modifier les tests.




Notre fonction est maintenant simple, propre et concise et les tests sont toujours verts. Notre refactor est donc réussit, on peut passer à la prochaine spécification.


Deuxième spécification


Nous devons maintenant faire en sorte de retourner Fizz si l'input est divisible par 3.



Le test échoue, on va maintenant gérer le cas du Fizz dans la fonction.




Le code le plus simple pour retourner  Fizz quand on a 3 et de tester si n === 3.


On va maintenant gérer un deuxième cas où on a un nombre divisible par 3.




Le test échoue, on met à jour le code.




Le code le plus simple pour retourner Fizzquand on a 3 ou 6 et de faire un ||, mais on se rend bien compte qu'on peut améliorer le code en utilisant le modulo de 3. Les tests sont tous verts, donc on peut se permettre de passer à la phase de refactor en modifiant uniquement le code de la production.



Refactor terminé, tous les tests sont verts, on peut passer à la prochaine spécification.


Troisième spécification


Nous devons maintenant faire en sorte de retourner Buzz si l'input est divisible par 5.


On va aller un peu plus vite, mais le procédé est le même, on va d'abord faire un test avec un input 5 puis 10, puis refactor.




Dernière spécification


Nous devons maintenant faire en sorte de retourner Fizz-Buzz si l'input est divisible par 3 et par 5.


Comme pour les spécifications précédentes, on va faire un test avec un input 15 puis 30 et enfin refactor.


Et voilà !


Le mot de la fin


Stop aux amalgames ! Le TDD n'est pas un outil pour avoir une bonne couverture de code avec les tests, ce n'est pas simplement "faire des tests". Le TDD est un outil dont le but est de guider le développeur vers un objectif. Il permet de donner un feedback régulier afin de s'assurer qu'on est toujours sur la bonne voie. Il faut le voir comme un GPS, qui nous donne des directions à suivre (tourner à droite, aller tout droit pendant 2km, etc.) jusqu'à atteindre un objectif.


Les dédicaces


Sommaire
Nos autres catégories
Notre newsletter tous les mois :
Je m'abonne
Merci ! C'est dans la boîte :)
Oops! Something went wrong while submitting the form.
Partager sur :
Nos autres catégories
Notre newsletter tous les mois :
Je m'abonne
Merci ! C'est dans la boîte :)
Oops! Something went wrong while submitting the form.
Partager sur :

Nos experts vous parlent
Le décodeur

Git : initiation, merge vs rebase, démo
22/6/2023

Origine


Git a été inventé et développé par Linus Torvalds en 2005. Il s’agit un logiciel libre et gratuit permettant aux développeurs de gérer les changements apportés au code au fil du temps. Linus Torvalds c’est aussi le petit génie qui est à l’origine du noyau Linux qu’il a commencé en 1991, donc bien avant Git.


Linux c’est un projet plutôt conséquent et il a donc dû nécessiter l’usage d’un outil de gestion de version. Cet outil, à l’époque, c’est BitKeeper. Le problème de BitKeeper, c’est qu’il s’agit d’un logiciel propriétaire et que toute la communauté qui gravite autour de Linux, elle n’aime pas vraiment les logiciels propriétaires. Alors que BitKeeper n’est déjà pas totalement apprécié par la communauté Linux, ils vont faire une annonce qui va déclencher la colère de toute cette communauté et surtout de Linus Torvalds. Ils vont cesser, du jour au lendemain, d’être gratuit. C’est à ce moment précis que Linus Torvalds décide de développer lui même son propre système de gestion de version du code source et tout comme Linux, ce système sera libre et gratuit.


Système de contrôle de version (VCS)


Git est donc un système de contrôle de version, il permet tout simplement de suivre l’évolution du code au fil du temps, à l’aide de branche, de fichiers et d’opérations sur ces fichiers.


Git est structuré comme suit :

  • on y retrouve des fichiers (le code source)
  • des branches (correspondant à une arborescence de fichiers)
  • et des opérations pour faire évoluer les fichiers dans les branches


Grâce à ces opérations, git permet de savoir qui a touché à quel fichier, à quel moment et comment.


GitHub, GitLab, Bitbucket, etc.


Git est un logiciel qui permet de sauvegarder et de gérer localement l’évolution du code source au fil du temps. GitHub, GitLab, Bitbucket, etc. sont des plateformes (web) qui se servent du logiciel git pour gérer le code source. Les dépôts ne sont alors plus gérer localement mais sur des serveurs distants et permettent donc notamment la collaboration avec plusieurs personnes. Ces plateformes proposent également de nombreuses fonctionnalités de gestion de projets et d’équipes (wiki, affectations de tâches, suivi des problèmes, roadmap, statistiques, etc.).

Les bases

Git Flow


Git Flow est une organisation de travail basé sur la capacité de Git à gérer des branches. Par défaut il existe une branche principale qui s’appelle main (anecdote : anciennement master, ce nom par défaut a changé pour des raisons culturelles, ne plus assimiler la notion de master/slave à l’industrie du développement face aux nombreux cas de racisme dans le monde). Il existe une deuxième branche que nous allons créer et qui sera également considérée comme principale, il s’agit de la branche develop.

Nous avons donc 2 branches principales :

  • main (anciennement master), qui représente le code source utilisé sur la production
  • develop, qui contient les dernières fonctionnalités dont la phase de développement est terminée

Tout au long du développement du projet, de nombreuses branches seront créées lors du développement des fonctionnalités et des corrections diverses. Ces branches respecteront des conventions de nommages comme suit :

  • feature/*, pour les branches de fonctionnalités
  • hotfix/* ou bugfix/*, pour les branches de corrections
  • refactor/*, pour améliorer la qualité du code

Conventional Commits


Conventional Commits est une spécification dont le but est d’améliorer la lisibilité des commits et l’historique des modifications du code source. À l’aide de ces conventions on peut identifier immédiatement le type, le contexte et l’objectif des modifications apportées au code sur un commit (nota bene : Cela permet aussi d’être compris par des outils automatisé pour générer de la documentation en autres).

Format Conventional Commits

Les types de commits les plus utilisés sont :

  • feat, développement d’une feature
  • fix, correction d’un bug
  • refactor, amélioration du code
  • test, ajout ou mise à jour de tests
  • chore, tâche technique non assimilée à une feature
  • remove, revert, style, ci, docs, etc.
Exemples de commits suivant la convention

Workflow : merge vs rebase


Avec Git Flow on travaille donc sur des branches partant de develop (ou autre) et une fois le travail terminé on met à jour develop pour qu’il ai connaissance des modifications apportées.

Il existe plusieurs façons de ramener les modifications d’une branche vers une autre. On peut utiliser la politique de merge ou bien la politique de rebase. Ces deux méthodes ont des avantages et des inconvénients.


Politique de merge


Lorsque le travail de développement est terminé sur une branche (de feature, de refactor, etc.), la branche contient un certain nombre d’opération qui n’existent pas sur la branche d’origine. Le principe de la politique de merge est simple : récupérer les modifications faites sur une branche et les ramener sur une autre branche qui n’a pas connaissance de ces modifications. Ces modifications sont ramenées telles quelles.

Avantages :

  • traçabilité totale, l’historique du code source correspond totalement à ce qui a été fait
  • résolution des conflits en une seule fois (peut être un inconvénient dans certains cas)


Inconvénients :

  • historique du code source vite pollué par des opérations inutiles “wip” ou des opérations qui s’annulent
  • historique peu fiable et difficile à debugger
  • résolution des conflits en une seule fois (peut être un avantage dans certains cas)


Politique de rebase


Lorsque le travail de développement est terminé sur une branche (de feature, de refactor, etc.), la branche a donc un certain nombre d’opération qui diverge de la branche principale. Lorsqu’on suit une politique de rebase, notre objectif va être de nettoyer ces opérations en les réécrivant jusqu’à avoir le nombre minimum d’opérations pertinentes.  

Avantages :

  • historique du code source linéaire et lisible qui peut servir de documentation
  • messages de commit clairs et respectant les conventions, plus de “wip”
  • plus de commits qui s’annulent et donc une fiabilité de l’historique
  • facilité pour revenir en arrière et trouver l’origin d’un bug car l’historique n’est pas pollué
  • facilité pour revoir une feature complète, pour la modifier ou l’annuler
  • résolution des conflits opération par opération (peut être un inconvénient dans certains cas)


Inconvénients :

  • demande une grande rigueur car on réécrit en permanence l’historique
  • demande une bonne communication ou des règles précises si on travaille en équipe sur la même branche
  • la réduction d’une nombre d’opération au minimum est parfois trop extrême et atténue la clarté du contexte dans certains cas
  • résolution des conflits opération par opération (peut être un avantage dans certains cas)


Nettoyage avec rebase Interactif


Avec la politique de rebase on réécrit l’historique des opérations faites sur le code source. Pour cela on peut utiliser des outils comme GitKraken ou autre, mais on peut également utiliser la commande git rebase interactive.


Les rebases réécrivent l’historique et donc écrasent totalement ce qui existait avant. En équipe il est donc indispensable de bien communiquer, de bien se mettre à jour et de prendre le soin de ne pas travailler sur la même branche. Si ces règles ne sont pas respecter, les pertes de code sont plus que probables !


Le processus classique de développement pour ne pas rencontrer de problèmes et profiter de la puissance du rebase est le suivant (en plus  :

  • travailler en local en faisant autant d’opérations que nécessaires
  • lorsque le travail est terminé et que tout fonctionne comme il faut, créer une pull request et demander une revue de code en gardant l’historique de code tel qu’il est pour garder du contexte et donner à l’auteur de la revue de code un moyen de comprendre le cheminement de pensé qui a amené à ces modifications
  • une fois la revue de code terminée et acceptée, il faut utiliser le rebase interactive pour nettoyer le code et ne garder que les opérations nécessaires
  • intégrer les modifications sur la branche d’origine et supprimer la branche créée précédemment

🧑💻 Démonstration


Supposons nous avons un dépôt git avec une seule branche main et un seul fichier hello.ts qui contient une fonction “Hello World !” comme suit :

Maintenant, nous devons développer la fonctionnalité “Good Bye World!”.

Pour cela, nous allons donc commencer par créer une branche qui respecte les conventions de nommage : git checkout -b feature/good-bye
Puis, nous allons créer un fichier good-bye.ts et écrire la fonction suivante :

Et nous allons créer un commit contenant cette fonctionnalité : git commit -m “feat: good bye world”.


Vous l’avez peut-être remarqué, une erreur s’est glissée dans la fonction, nous allons donc faire un commit pour la corriger :

Avec le commit suivant : git commit -m “fix(good-bye): typo”.

Nous avons donc 2 commits alors qu’il serait plus pertinent d’en avoir qu’un seul. Nous allons donc utiliser le git rebase interactive pour réécrire l’historique des modifications.

Pour initialiser le rebase interactive on utilise la commande suivante : git rebase interactive HEAD~2

Cette commande va ouvrir l’interface suivante :

Sur cette interface on voit les 2 derniers commits de mon dépôt git (parce qu’on a utilisé HEAD~2). On y retrouve également une documentation des commandes qu’on peut utiliser devant l’identifiant de chaque commit.


Dans notre cas, on veut fusionner les modifications du deuxième commit avec le premier commit. C’est donc la commande fixup qui nous intéresse, nous allons donc remplacer pick devant le commit fix(good-bye): typo par fixup.


On enregistre et on obtient l’historique suivant (un seul commit) :

7f077f4 (HEAD -> feature/good-bye-world) feat: good bye world

Liens utiles

Laravel vs Symfony : quel Framework choisir ? Le guide
4/12/2023

Bienvenue dans l'univers du développement web, où chaque choix compte. Laravel et Symfony, deux géants du langage serveur PHP, se disputent la première place. Dans cet article, nous plongeons dans les détails, examinant performances, sécurité, et courbes d'apprentissage.

Prêts à choisir le Framework idéal pour votre projet ? 

Laravel et Symfony : Les titans du développement PHP

Dans l'arène du développement serveur PHP, deux titans émergent : Laravel et Symfony.

Laravel, créé par Taylor Otwell, séduit par son architecture élégante basée sur le modèle MVC (pour Modèle-Vue-Contrôleur), ses outils puissants, et une courbe d'apprentissage conviviale.

De l'autre côté, Symfony, de SensioLabs, brille par sa modularité et des composants réutilisables. Avant de choisir votre champion, explorons en détail ce que chaque Framework peut vous offrir.

Laravel

Laravel est un Framework PHP qui a su captiver la communauté des développeurs. Imaginé par le développeur américain, Taylor Otwell, Laravel séduit par son architecture claire et élégante, basée sur le modèle architectural MVC. Cette structure facilite la conception d’applications et le développement en offrant une séparation nette entre les composants d’un projet.

Laravel ne fait pas que promettre, il délivre des performances remarquables. Les temps de réponse sont optimisés pour offrir une expérience utilisateur plus fluide. La gestion intelligente des bases de données et les outils intégrés font de Laravel un choix judicieux pour des applications performantes.

Dans le monde numérique d'aujourd'hui, la sécurité est primordiale. Pour cela, Laravel intègre des systèmes robustes garantissant la protection à long terme de vos applications web modernes. Des mécanismes de sécurité avancés sont mis en place pour contrer les menaces potentielles.

La courbe d'apprentissage est souvent un obstacle, mais pas avec Laravel. Son approche conviviale permet aux développeurs de rapidement maîtriser le Framework. La documentation détaillée et la communauté active font de l'apprentissage une expérience fluide.

Exemple de code concret : Gestion des utilisateurs avec Laravel

Une image contenant texte, capture d’écran, logiciel, multimédiaDescription générée automatiquement

  • Laraval utilise l’ORM (pour Object Relational Model) Eloquent pour simplifier les opérations d’interactions avec la base de données.
  • La fonction bcrypt () sécurise le mot de passe avant son enregistrement.
  • La réponse est renvoyée au format JSON.

Laravel est votre allié dans la création d'applications web dynamiques et performantes.

Symfony

L’univers du Framework PHP Symfony est tout aussi passionnant. Renommé, riche et modulaire, Symfony est développé et maintenu par la société française SensioLabs. Symfony se démarque par son approche modulaire et ses composants réutilisables, offrant une flexibilité remarquable dans la conception d’application web.

Symfony ne se contente pas d'une architecture flexible, il excelle également en matière de performances. Les temps de réponse compétitifs et la gestion efficace des ressources font de Symfony un choix judicieux pour réaliser des applications PHP performantes.

La sécurité étant une priorité dans le paysage numérique actuel, Symfony intègre des mécanismes de sécurité assurant la protection de vos applications contre les menaces. La courbe d'apprentissage de Symfony est également conçue pour faciliter l'adoption du Framework. Les développeurs bénéficient d'une documentation exhaustive et d'une communauté active, simplifiant ainsi l'exploration des fonctionnalités avancées de Symfony.

Exemple de code concret : gestion des utilisateurs avec Symfony

Une image contenant texte, capture d’écranDescription générée automatiquement

  • Symfony utilise l’ORM Doctrine ainsi que les méthodes setName(), setEmail(), et setPassword() pour définir les propriétés de l'objet utilisateur.
  • Le mot de passe est sécurisé à l’aide de password_hash().
  • La méthode flush() persiste les changements en base de données.
  • La réponse est renvoyée au format JSON.

En somme, Symfony est un partenaire fiable pour le développement web.

Comparaison directe

Plongeons dans la comparaison directe entre Laravel et Symfony, examinant de près les éléments clés qui pourraient influencer votre décision dans le choix du Framework PHP idéal.

Performance : la quête de la réactivité

La performance est cruciale pour une expérience utilisateur optimale. Nous comparons les temps de réponse de Laravel et Symfony pour vous aider à choisir le Framework qui offre la réactivité nécessaire à votre projet.

  • Temps de Réponse : Laravel excelle avec des temps de réponse moyens de 250 millisecondes, offrant une expérience utilisateur rapide. Symfony, bien que compétitif, tend à se situer légèrement au-dessus, aux alentours de 300 millisecondes.
  • Exemple : Un site e-commerce nécessitant une navigation rapide pourrait bénéficier des performances de Laravel, assurant une réponse quasi-instantanée lors de la recherche de produits.

Architecture : MVC sous le microscope

L'architecture MVC (Modèle-Vue-Contrôleur) est le pilier du développement web moderne. Analysons comment Laravel et Symfony abordent cette structure fondamentale et en quoi cela peut impacter la gestion de votre code.

  • Laravel : Son modèle MVC clair offre une structure fluide. Les développeurs peuvent facilement segmenter les responsabilités, améliorant la maintenabilité du code.
  • Symfony : Une approche modulaire permettant une personnalisation avancée. Les composants réutilisables simplifient le développement, mais cela peut entraîner une complexité accrue pour les petits projets.
  • Exemple : Un projet nécessitant une structure claire et simple pourrait préférer Laravel, tandis qu'un système complexe pourrait tirer parti de la modularité de Symfony.

Command Line Interface (CLI) : L'art de la ligne de commande

La ligne de commande est l'alliée du développeur moderne. Comparons comment Laravel et Symfony s'engagent dans le royaume de l'interface en ligne de commande (CLI), facilitant ainsi le flux de travail du développeur.

  • Laravel : La CLI de Laravel simplifie les tâches courantes telles que la génération de code, facilitant le flux de travail du développeur.
  • Symfony : Une CLI robuste, mais avec une courbe d'apprentissage plus prononcée. Elle offre une personnalisation avancée, mais peut sembler complexe pour les débutants.
  • Exemple : Pour un développeur qui privilégie la simplicité et la rapidité, la CLI de Laravel serait un choix naturel.

Migration de base de données : Gestion intelligente des données

La migration efficace de la base de données est un défi incontournable. Jetons un regard attentif sur la manière dont Laravel et Symfony simplifient ce processus critique pour garantir une gestion fluide des données.

  • Laravel : Les migrations simplifient les changements de schéma de base de données. Les développeurs peuvent versionner et partager ces modifications facilement.
  • Symfony : Des migrations intégrées offrant également une gestion efficace des changements de base de données. Les bundles Symfony facilitent la modularité.
  • Exemple : Un projet nécessitant des mises à jour fréquentes de la base de données pourrait bénéficier de la simplicité des migrations de Laravel.

N’hésitez pas à plonger davantage dans ces détails pour faire un choix éclairé entre Laravel et Symfony, en alignant les forces de chaque Framework avec les exigences spécifiques de votre projet.

Frameworks PHP : Un choix déterminant pour le développement web

Explorez l'impact déterminant que le choix entre Laravel et Symfony peut avoir sur votre projet de développement web.

Choisir le bon Framework : un dilemme crucial

La sélection du bon Framework PHP est un dilemme majeur, influençant directement la réussite de votre projet. Laravel et Symfony offrent des avantages uniques, mais lequel répond le mieux à vos besoins spécifiques ?

  • Laravel : Excellant dans la simplicité et la rapidité de développement, Laravel est souvent le choix préféré pour les projets de taille moyenne nécessitant une mise en œuvre rapide.
  • Symfony : Connu pour sa modularité et ses composants réutilisables, Symfony brille dans des projets plus complexes où la personnalisation et la réutilisation de code sont essentielles.
  • Exemple : Un site vitrine pour une petite entreprise pourrait privilégier la simplicité de Laravel, tandis qu'un système de gestion complexe opterait probablement pour la modularité de Symfony.

Forces et faiblesses : un examen critique

Chacun des Frameworks a ses points forts et ses faiblesses. Examinons en détail les avantages et les limitations de Laravel et Symfony pour vous guider dans cette décision cruciale.

  • Laravel : Points forts dans la simplicité, la documentation claire et la courbe d'apprentissage conviviale. Cependant, sa modularité peut être moins flexible pour des projets très spécifiques.
  • Symfony : Excellant dans la modularité, Symfony offre une flexibilité maximale, mais sa courbe d'apprentissage peut être plus abrupte pour les débutants.
  • Exemple : Un projet nécessitant des fonctionnalités spécifiques pourrait bénéficier de la flexibilité de Symfony, tandis qu'un projet plus simple pourrait opter pour la simplicité de Laravel.

Taille du projet : l'importance du contexte

La taille de votre projet est un facteur déterminant. Nous évaluons comment Laravel et Symfony s'adaptent aux projets de différentes envergures, vous permettant ainsi de choisir en fonction de vos besoins spécifiques.

  • Laravel : Idéal pour des projets de taille moyenne, où la rapidité de développement est cruciale.
  • Symfony : Convient particulièrement aux projets de grande envergure, offrant la modularité nécessaire pour gérer la complexité.
  • Exemple : Un blog personnel pourrait tirer parti de la simplicité de Laravel, tandis qu'un système de gestion d'entreprise complexe pourrait profiter de la robustesse de Symfony.

Performance attendue : Une réflexion essentielle

Quelles performances attendez-vous de votre application web ? Laravel et Symfony offrent des approches différentes. Décortiquons ces approches pour vous aider à prendre une décision éclairée.

  • Laravel : Performances élevées pour des projets de taille moyenne, avec des temps de réponse compétitifs.
  • Symfony : Excellentes performances également, mais peut être plus adapté à des projets plus vastes.
  • Exemple : Un site de portfolio artistique pourrait privilégier les performances de Laravel, tandis qu'un site e-commerce à fort trafic pourrait opter pour Symfony.

Préférences des développeurs : Un facteur humain

L'adhésion des développeurs à un Framework peut être un critère déterminant. Découvrez comment Laravel et Symfony sont perçus dans la communauté des développeurs, un aspect clé pour le succès à long terme de votre projet.

  • Laravel : Apprécié pour sa simplicité et son approche élégante, attirant souvent les développeurs cherchant une courbe d'apprentissage plus douce.
  • Symfony : Adopté par des développeurs expérimentés cherchant une modularité étendue et une personnalisation avancée.
  • Exemple : Une équipe de jeunes développeurs débutants pourrait se sentir à l'aise avec la simplicité de Laravel, tandis qu'une équipe expérimentée pourrait être attirée par la puissance de Symfony.

Après avoir exploré les nuances de Laravel et Symfony, il est temps de conclure. Chaque projet a des besoins uniques, et votre décision devrait être ancrée dans une évaluation minutieuse des forces et des faiblesses de chaque Framework.

En conclusion, gardez un œil sur l'horizon du développement web. Les Frameworks évoluent, les tendances changent. Votre choix devrait également être orienté vers l'avenir, en tenant compte des mises à jour futures, de la communauté active et des innovations à venir.

Votre choix entre Laravel et Symfony déterminera la trajectoire de votre projet. Explorez les possibilités infinies offertes par ces Frameworks PHP et prenez votre décision en connaissance de cause. Votre aventure de développement web commence ici. Bonne exploration !

L’Architecture Hexagonale sur un projet Web + Mobile (Partie 2 sur 5)
28/2/2024

Dans l'article précédent nous avons initialisé notre monorepo, la CI, le framework de test et préparé la structure de notre projet et plus précisément de notre Architecture Hexagonale pour la lib core.

Dans ce nouvel article de la série notre objectif va être de mettre en place l'Architecture Hexagonale et de montrer comment grâce à elle nous allons pouvoir développer et créer de la logique métier sans UI (donc sans ouvrir le navigateur ou l'app mobile). Pour cela nous allons travailler en TDD (Test-Driven Development, vous pouvez voir mon article à ce sujet) et utiliser le feedback des tests.

L'Architecture Hexagonale

La structure cible

Pour rappel, voici la structure que l'on va mettre en place à l'issue de cet article :

- src
    - wallet
       - __ tests __
         - wallet.service.test.ts
       - domain
         - wallet.ts
         - wallet.repository.ts
         - wallet.service.ts
       - infrastructure
         - in-memory-wallet.repository.ts
         - local-storage-wallet.repository.ts
         - mmkv-wallet.repository.ts
       - user-interface
         - wallet.store.ts

Chose promise, chose due ! Nous allons maintenant rentrer dans le détail de chaque fichier, à quoi ils servent et ce qu'ils contient.

Développer en TDD

Lorsqu'on travaille en TDD on commence par le test et ce test va nous guider vers un objectif. Il va nous assurer qu'on suit le bon chemin à l'aide de la boucle de feedback régulière qu'on obtient à l'aide des tests. Pour en savoir plus sur la méthodologie à suivre pour faire du TDD je vous invite à nouveau à lire mon article à ce sujet.

Nous allons commencer par travailler sur l'entité Wallet qui correspond à un portefeuille qui a un solde négatif ou positif (par exemple on peut avoir le portefeuille "Compte Principal Julien" qui a un solde positif de 1000€).

Voici les tests mis en place pour cette entité :

describe('Wallet Service', () => {
	let service: WalletService

	beforeEach(() => {
		const repository = new InMemoryWalletRepository()
		service = new WalletService(repository)
	})

	test('getAll > should retrieve all wallets', async () => {
		const newWallet = { id: '1', name: 'Wallet 1', balance: 0 }

		await service.create(newWallet)
		const retrievedWallets = await service.getAll()

		expect(retrievedWallets).toEqual([newWallet])
	})

	test('get > should retrieve a wallet according to an id', async () => {
		const newWallet = { id: '1', name: 'Wallet 1', balance: 0 }

		await service.create(newWallet)
		const retrievedWallet = await service.get(newWallet.id)

		expect(retrievedWallet).toEqual(newWallet)
	})

	test('create > shoudl create a wallet', async () => {
		const newWallet = { id: '1', name: 'Wallet 1', balance: 0 }

		const createdWallet = await service.create(newWallet)
		const retrievedWallets = await service.getAll()
		const retrievedWallet = await service.get(createdWallet.id)

		expect(createdWallet).toEqual(newWallet)
		expect(retrievedWallets).toEqual([newWallet])
		expect(retrievedWallet).toEqual(newWallet)
	})

	test('update > should update the specified wallet', async () => {
		const newWallet = { id: '1', name: 'Wallet 1', balance: 0 }
		const updatedWallet = { id: '1', name: 'Wallet 1', balance: 100 }

		await service.create(newWallet)
		const retrievedWallet = await service.get(newWallet.id)
		const modifiedWallet = await service.update(updatedWallet)
		const retrievedModifiedWallet = await service.get(modifiedWallet.id)

		expect(retrievedWallet).toEqual(newWallet)
		expect(modifiedWallet).toEqual(updatedWallet)
		expect(retrievedModifiedWallet).toEqual(updatedWallet)
	})

	test('delete > should delete a wallet according to an id', async () => {
		const newWallet = { id: '1', name: 'Wallet 1', balance: 0 }

		await service.create(newWallet)
		const retrievedWallet = await service.get(newWallet.id)
		await service.delete(newWallet.id)
		const retrievedWallets = await service.getAll()

		expect(retrievedWallet).toEqual(newWallet)
		expect(retrievedWallets).toEqual([])
	})
})

On peut comprendre via ces tests que les cas d'utilisations de notre entité sont :

  • getAll, récupération de tous les portefeuilles
  • get, récupération d'un portefeuille en particulier
  • create, création d'un portefeuille
  • update, mise à jour d'un portefeuille
  • delete, suppression d'un portefeuille

Nous allons voir maintenant comme réussir à mettre en place ces tests.

Domain

Nous allons commencé par créer le contenu de la partie Domain. Dans cette partie nous allons retrouver tout ce qui représente le problème à résoudre (problème métier). C'est une partie qui doit être totalement indépendante.

L'entité

Commençons par créer notre entité Wallet correspondant à un portefeuille.

type Wallet = {
	// un identifiant unique (ex: 4d0c2e72-be1a-4e2c-a189-2f321fcdc3a4)
	id: string

	// un nom (ex: Compte Principal Julien)
	name: string

	// un nombre positif ou négatif pour le solde (ex: +1000€)
	balance: number
}


Le repository

Maintenant que notre entité est définie, nous allons définir une interface que l'on appelle également port qui va préciser comment interagir avec cette entité. Nous utilisons ici un modèle de conception d'inversion de dépendances qui nous permet de rester totalement libre sur les outils à utiliser pour respecter cette interface. Nous pourrons très bien implémenté cette interface en utilisant une base de données, une API ou un localStorage par exemple, le domaine s'en fiche.

interface WalletRepository {
	getAll(): Promise<Wallet[]>
	get(walletId: string): Promise<Wallet | undefined>
	create(wallet: Wallet): Promise<Wallet>
	update(wallet: Wallet): Promise<Wallet>
	delete(walletId: string): Promise<void>
}

Le service

Nous avons notre entité et nous savons commencer interagir avec, maintenant nous allons créer un service qui va consumer une implémentation du de notre interface repository (partie suivante dans l'infrastructure).

class WalletService implements WalletRepository {
	constructor(private repository: WalletRepository) {}

	getAll() {
		return this.repository.getAll()
	}

	get(walletId: string) {
		return this.repository.get(walletId)
	}

	create(wallet: Wallet) {
		return this.repository.create(wallet)
	}

	update(wallet: Wallet) {
		return this.repository.update(wallet)
	}

	delete(walletId: string) {
		return this.repository.delete(walletId)
	}
}

Infrastructure

L'infrastructure est composée des différentes implémentations des ports du domaine, on les appelle également Adapters. Ici, nous aurons du code spécifique pour consommer une technologie concrète (une base de données, une API, etc.). C'est une partie qui ne doit dépendre uniquement du domaine.

L'implémentation du repository

Nous allons maintenant voir l'une des implémentation possible de notre WalletRepository. Pour commencer nous allons faire du in-memory, pratique notamment pour la mise en place des premiers tests de nos cas d'utilisations.

class InMemoryWalletRepository implements WalletRepository {
	private wallets: Wallet[] = []

	getAll() {
		return Promise.resolve(this.wallets)
	}

	get(walletId: string) {
		return Promise.resolve(this.wallets.find((wallet) => wallet.id === walletId))
	}

	create(wallet: Wallet) {
		this.wallets.push(wallet)
		return Promise.resolve(wallet)
	}

	update(wallet: Wallet) {
		const index = this.wallets.findIndex((w) => w.id === wallet.id)
		this.wallets[index] = wallet
		return Promise.resolve(wallet)
	}

	delete(walletId: string) {
		const index = this.wallets.findIndex((w) => w.id === walletId)
		this.wallets.splice(index, 1)
		return Promise.resolve()
	}
}

Comment dis précédemment, il s'agit d'une des multiples implémentation possible de notre WalletRepository. Nous pouvons très bien imaginer plus tard mettre en place un LocalStorageWalletRepository ou bien un SupabaseWalletRepostory.

Vous pouvez consulter mon répertoire public de broney sur GitHub pour voir mon implémentation de ces 2 repository et notamment de comment j'ai adapté ma série de test pour garantir leur bon fonctionnement.

User Interface

La partie user interface est composée de tous les adaptateurs qui constituent les points d'entrée de l'application. Les utilisateurs utilisent ces adaptateurs pour pouvoir interagir avec le coeur de l'application. Dans notre cas nous allons régulièrement utiliser des stores en utilisant la libraire Zustand. Il s'agit d'une libraire JS minimaliste pour la gestion d'états (une solution plus complexe serait par exemple Redux).

Voyons voir comment articuler notre store Zustand pour permettre à l'utilisateur d'interagir avec le coeur de l'application.

import { createStore } from 'zustand/vanilla'
import { InMemoryWalletRepository } from '../infrastructure/in-memory-wallet.repository'
import { WalletService } from '../domain/wallet.service'
import { Wallet } from '../domain/wallet'

const repository = new InMemoryWalletRepository()
const service = new WalletService(repository)

type States = {
	wallets: Wallet[]
	currentWallet: Wallet | undefined
}

type Actions = {
	load: () => void
	setCurrentWallet: (wallet: Wallet) => void
	getWallet: (walletId: string) => void
	createWallet: (wallet: Wallet) => void
	updateWallet: (wallet: Wallet) => void
	deleteWallet: (walletId: string) => void
}

export const walletStore = createStore<States & Actions>()((set) => ({
	wallets: [],
	currentWallet: undefined,

	load: async () => {
		const allWallets = await service.getAll()
		set({ wallets: allWallets })
	},

	setCurrentWallet: (wallet) => set({ currentWallet: wallet }),

	getWallet: async (walletId: string) => {
		const wallet = await service.get(walletId)
		set({ currentWallet: wallet })
	},

	createWallet: async (wallet: Wallet) => {
		const newWallet = await service.create(wallet)
		set((state) => ({ wallets: [...state.wallets, newWallet] }))
	},

	updateWallet: async (wallet: Wallet) => {
		const updatedWallet = await service.update(wallet)
		set((state) => ({
			wallets: state.wallets.map((w) => (w.id === updatedWallet.id ? updatedWallet : w)),
			currentWallet: state.currentWallet?.id === updatedWallet.id ? updatedWallet : state.currentWallet,
		}))
	},

	deleteWallet: async (walletId: string) => {
		await service.delete(walletId)
		set((state) => ({
			wallets: state.wallets.filter((w) => w.id !== walletId),
			currentWallet: state.currentWallet?.id === walletId ? undefined : state.currentWallet,
		}))
	},
}))


Avec ce store on remarque qu'on va pouvoir facilement, dans n'importe quel environnement JavaScript, charger, définir, récupérer, créer, mettre à jour et supprimer des portefeuilles, tout en maintenant un état global pour l'ensemble des portefeuilles et du portefeuille courant.

Conclusion

Nous avons maintenant terminé ce deuxième article de cette série sur le développement d'une application web et mobile avec l'Architecture Hexagonale et le partage de la logique métier et des composants UI.

Dans cette deuxième partie nous avons vu comment travailler en TDD et surtout comment écrire de la logique métier sans avoir à ouvrir une quelque interface à l'exception du terminal pour les retours de tests.

Nous avons également eu un aperçu de comment nous allons interagir avec nos applications avec le coeur de l'application, via notre store Zustand. Nous irons plus loin à ce sujet dans le prochain article, la troisième partie : Partager de la logique métier et des composants entre le Web et le Mobile.

Échangeons
sur votre projet !

Nous contacter

Simulateur

Bienvenue dans le
simulateur d’estimation

Sélectionnez
vos besoins

Sélectionnez un ou plusieurs choix

Définissez les
fonctionnalités

Sélectionnez un ou plusieurs choix

Dernière
étape !

Renseignez votre adresse mail pour recevoir l’estimation !
Obtenez l’estimation
Précédent
Suivant

Bravo ! Vous avez terminé
l’estimation de votre future app !

Vous recevrez dans votre boite mail l’estimation personnalisé. Une estimation vous offre la possibilité de vous projeter dans un budget, vous permettant ainsi de planifier en toute confiance. Néanmoins, chez Yield, nous adoptons une approche agile, prêts à remettre en question et ajuster nos évaluations en fonction de l'évolution de vos besoins et des spécificités de votre projet.
Retour au site
Oops! Something went wrong while submitting the form.