Qualité de développement

BUT2-R3.04 - année 2022

IUT Aix-Marseille logo

Qui suis-je ?

Sébastien HOUZÉ

Logo Twitter Logo Github Logo LinkedIn

20 ans d'XP, de développeur à CTO en passant par responsable qualité logicielle.

Logo Rue du commerce Logo VeryLastRoom Logo Onatera Logo Gogaille

Qui êtes-vous ?

⏳ 5 min chit-chat

Introduction

Pourquoi

La qualité logicielle ?

3 principaux enjeux*

  1. Stratégique

  2. Économique

  3. Compétitivité

*D'autant plus importants et liés à la durée de vie du logiciel

Enjeu Stratégique

Parce que ne pas en faire, c'est le meilleur moyen de multiplier les bugs et de perdre des clients ou utilisateurs d'une l'application.

Enjeu Économique

Parce que sinon le coût de maintenance d'une ligne de code explose.

Cost per line of code over time

Enjeu de Compétitivité

Parce qu'une équipe qui pratique la qualité augmente sa capacité à produire de nouvelles fonctionnalités appréciées par les utilisateurs, plus rapidement.

Où peut-on intervenir ?

SDLC

*Software Development LyfeCycle

Conception
Maintenance
Évolution

Qu'est-ce qui peut vous alerter sur un problème de qualité à chacune de ces étapes ?

Conception

Vous avez du mal à :

  • Comprendre le code que vous avez écrit il y a quelques jours ou semaines.
  • Vous faire aider par un collègue car il ne comprend pas votre code.
  • Vous assurer que vous couvrez bien la fonctionnalité demandée.

Maintenance

Vous avez du mal à :

  • Débugger votre application.
  • Modifier une fonctionnalité sans modifier des endroits dans le code qui ne devraient pas être impactés.
  • Vous souvenir de ce que fait votre code par rapport à la fonctionnalité à maintenir.

Évolution

Vous avez du mal à :

  • Ajouter une nouvelle fonctionnalité.
  • Mettre des frontières entre les différentes fonctionnalités de votre application.
  • Décider où mettre une nouvelle fonctionnalité.

Ce module doit vous aider à...

... écrire un code propre et compréhensible.

Code propre

En conclusion de ce module, nous vérifierons qu'il est :

  1. Facile à faire évoluer, on comprend qui est responsable de quoi
  2. Organisé en modules découplés
  3. L'expression la plus pure du métier qu'il réalise

Code compréhensible

en conclusion de ce module, nous vérifierons qu'il :

  1. Favorise toujours l'explicite à l'implicite
  2. Suit des règles de conception
  3. Est documenté par ses cas d'usage

À la fin de ce module :

  1. Les règles élémentaires de la programmation fonctionnelle
  2. La création d'un objet et de ses dépendances
  3. La gestion de la relation entre objets
  4. La gestion des responsabilités et comportements d'un objet

N'aurons plus de secrets pour vous !

Pourquoi La qualité logicielle ?

Ce qu'il faut retenir

  • La qualité logicielle apporte des garanties économiques et stratégiques.
  • Dans les faits, Qualité = Vélocité. Vous ne pouvez être totalement convaincu que par la mise en pratique.

Pourquoi La qualité logicielle ?

Pour aller plus loin

Une partie de la qualité logicielle puise son inspiration dans les processus industriels tels que Six Sigma (1986, Motorola) ou encore Toyota Way/Lean/Kanban (2001)

ISO/IEC 9126 est un standard international qui définit les critères de qualité logicielle, articulés autour de 6 axes (testabilité, fiabilité, sécurité, maintenabilité, portabilité, utilisabilité)

L'agilité est une approche de développement logiciel qui prône la production de valeur par le logiciel à la vélocité la plus constante possible, sans sacrifier la qualité. Voir, eXtreme Programming (1999), Scrum (1986), Kanban (2003), Software Craftsmanship (2008),

Plan

  1. Maux et remèdes : dette et refactoring
  2. programmation fonctionnelle & immuabilité
  3. Améliorez une application avec SOLID
  4. Détectez une application STUPID
  5. Design Patterns
  6. Ajoutez du contrôle qualité
  7. Rendez votre documentation vivante

Maux et remèdes

Dette et Refactoring

La dette technique

La dette technique est un concept du développement logiciel inventé par Ward Cunningham en 1992. Le terme vient d'une métaphore, inspirée du concept existant de dette dans le domaine des finances et des entreprises, appliquée au domaine du développement logiciel. [Wikipedia]

La dette technique

Dans la vie

La dette technique

Définition plus personnelle

La dette technique c’est le fait d’ajouter de la complexité à un projet engendré par des décisions techniques qui entraînent la livraison d’un logiciel de faible qualité.

Qui dit dette dit qu'on peut en principe décider (éviter de subir) quand on s'endette. Cela signifie aussi qu'on peut rembourser et décider plus ou moins des modalités de remboursement.

La dette technique

Engendrée par la complexité accidentelle

Remèdes

Refactoring, replatforming, architecture logicielle...

Disclaimer: dans ce module nous nous concentrons sur les bases et donc sur les méthodes qui aident à régler les problèmes généraux de conception, y compris sur du code hérité (dit legacy).

Pour la suite, nous partons du principe que vous êtes toujours face à du code legacy car c'est encore plus intéressant pour traiter les problématiques de conception. C'est pourquoi nous allons nous concentrer sur le refactoring.

Remèdes

Refactoring

Le refactoring, c'est un processus d'amélioration du code sans créer de nouvelle fonctionnalité dans le but de transformer du code de faible qualité en code propre, au design simplifié.

Refactoring

La règle de 3

          %%{init: {'theme': 'dark', 'themeVariables': { 'darkMode': true }}}%%
          flowchart LR
						LC[Legacy Code] --> |Refactoring|RC[Clean Code]
        
  1. Quand vous écrivez du code pour la 1ère fois, faites le simplement.
  2. Lorsque vous faites quelque chose d'identique pour la 2ème fois, dégoûtez-vous de devoir répéter mais faites quand même la même chose.
  3. Lorsque vous faites quelque chose pour la troisième fois, commencez à le remanier (refactoring).

Refactoring

Quand vous ajoutez une fonctionnalité

Le remaniement vous aide à comprendre le code d'autres personnes. Si vous devez traiter le code sale de quelqu'un d'autre, essayez d'abord de le remanier. Un code propre est beaucoup plus facile à appréhender. Vous l'améliorerez non seulement pour vous-même, mais aussi pour ceux qui l'utiliseront après vous.

Le remaniement facilite l'ajout de nouvelles fonctionnalités. Il est beaucoup plus facile d'apporter des modifications dans un code propre.

Refactoring

Quand vous corrigez un bug

Les bugs dans le code se comportent exactement comme ceux de la vie réelle : ils vivent dans les endroits les plus sombres et les plus sales du code. Nettoyez votre code et les erreurs se découvriront pratiquement d'elles-mêmes.

Le remaniement proactif élimine la nécessité de tâches de remaniement spéciales par la suite. participez à instaurer le bien-être dans votre équipe !

Refactoring

Pendant une revue de code

La revue de code peut être la dernière chance de mettre de l'ordre dans le code avant qu'il ne soit livré en production.

Il est préférable d'effectuer ces revues en binôme avec un auteur. Ainsi vous pourrez résoudre rapidement les problèmes simples et mesurer le temps nécessaire à la résolution des problèmes plus difficiles.

Dette et Refactoring

Ce qu'il faut retenir

  • Il faut chasser la complexité accidentelle pour limiter la dette. Il faut savoir faire des compromis et contracter intentionnellement de la dette. Idéalement il faut une trace de cette dette contractée et un plan pour la rembourser. Cela demande de l'expérience et de la discipline, et un peu d'outils.
  • Le code est vivant : le refactoring est une pratique d'amélioration continue qui permet de viser la qualité. D'ailleurs on dit que le code qui est rarement édité "pourrit".

Dette et Refactoring

Pour aller plus loin

Stateless

Programmation fonctionnelle & immuabilité

Functional Programming

Définition

La programmation fonctionnelle puise ses origines dans les mathématiques. C'est un paradigme de programmation qui traite les fonctions comme des valeurs de première classe.

OK... Ne fuyez pas !

En Functional Programming (FP), les fonctions réalisent des opérations qui ne produisent pas d'effets secondaires (ou side effects).

Fonction Pure

LE concept clé, le seul que nous allons vraiment prendre le temps de voir.

							
          <?php
          $globalCounter = 0;
          function increment(int $amount): int { // Fonction impure
              $GLOBALS['globalCounter'] += $amount;

              return $GLOBALS['globalCounter'];
          }

          echo increment(1)."\n"; // Affiche 1

          $globalCounter = 0;
          echo (2 * increment(1))."\n"; // Affiche 2

          $globalCounter = 0;
          echo (increment(1) + increment(1))."\n"; // Affiche 3, Oups!

          // Fonction pure
          function fp_increment(int $counter, int $amount): int {
              return $counter + $amount;
          }

          // Affiche 1
          echo fp_increment(0, 1)."\n";

          // Affiche 2, on compose plutôt qu'additionner
          echo (fp_increment(fp_increment(0, 1), 1))."\n";
							
            

immuabilité

Objet immuable : dont l'état ne peut pas être modifié après sa création.

							
              <?php
              // On est ajd le 7 décembre 2022

              // Exemple de non immuabilité (on dit mutable)
              $today = new Datetime('today');
              echo $today->format('Y-m-d')."\n"; // Affiche 2022-12-07

              $tomorrow = $today->modify('+1 day');
              echo $today->format('Y-m-d')."\n"; // Affiche 2022-12-08
              echo $tomorrow->format('Y-m-d')."\n"; // Affiche 2022-12-08








              // On est ajd le 7 décembre 2022
              // Exemple d'immuabilité (on dit immutable)
              $today = new DatetimeImmutable('today');
              echo $today->format('Y-m-d')."\n"; // Affiche 2022-12-07

              $tomorrow = $today->modify('+1 day');
              echo $today->format('Y-m-d')."\n"; // Affiche 2022-12-07
              echo $tomorrow->format('Y-m-d')."\n"; // Affiche 2022-12-08
							
              

Programmation fonctionnelle & immuabilité

Ce qu'il faut retenir

  • Les variables globales et le partage d'état dans le code c'est très souvent le mal.
  • L'immuabilité et les fonctions pures permettent d'avoir du code simple à tester et sans effet de bord.

Programmation fonctionnelle & immuabilité

Pour aller plus loin

Améliorer une application

Respectez les principes SOLID

SOLID ?

Robert C. Martin l'a introduit en 2002 dans son livre Agile Software Development, Principles, Patterns and Practices.

L'acronyme SOLID est un moyen mnémotechnique pour retenir 5 grands principes de conception d'application qui rendent plus facile à comprendre, à maintenir et faire évoluer.

Les 5 principes SOLID

  1. S comme Single Responsibility Principle
  2. O comme Open-Closed Principle
  3. L comme Liskov Substitution Principle
  4. I comme Interface Segregation Principle
  5. D comme Dependency Inversion Principle

S comme Single Responsibility Principle

Une classe ne devrait avoir qu'une et une seule raison de changer.

L’idée ici est de faire en sorte qu’une classe ne soit responsable que d’une seule fonction de votre application, et que cette responsabilité soit complètement encapsulée ("cachée") dans la classe.

Objectif : réduire la complexité de votre projet.

SRP en pratique

Imaginons que vous ayez une classe Car qui représente une voiture. Cette classe a plusieurs responsabilités : elle gère la vitesse, la direction, etc..

Si vous souhaitez ajouter une nouvelle fonctionnalité, par exemple la gestion de la consommation de carburant, vous allez devoir modifier cette classe. Et si vous souhaitez ajouter une nouvelle fonctionnalité qui n'a rien à voir avec la voiture, par exemple l'envoi d'un SMS à chaque fois que la voiture roule, vous allez encore devoir modifier cette classe.

Cela va rendre votre classe très complexe et difficile à maintenir. Voyez donc SRP comme une façon de vous aider à déléguer proprement les responsabilités et faire collaborer vos classes qui chacune assume des responsabilités spécifiques.

SRP par l'exemple

							
							<?php
							Class CsvDataImporter {
								public function __construct(private Db $db) {}

								public function import(string $filename): void
								{
									$records = $this->loadFile($filename);

									$this->importData($records);
								}
							}

							private function loadFile(string $filename): array
							{
									$records = [];
									if (false !== $handle = fopen($filename, 'r')) {
											while ($record = fgetcsv($handle)) {
													$records[] = $record;
											}
									}
									fclose($handle);

									return $records;
							}

							private function importData(array $records)
							{
									try {
											$this->db->beginTransaction();
											foreach ($records as $record) {
													$stmt = $this->db->prepare('INSERT INTO ...');
													$stmt->execute($record);
											}
											$this->db->commit();
									} catch (PDOException $e) {
											$this->db->rollback();
											throw $e;
									}
							}
							
						

Quelles sont les 2 responsabilités de cette classe ?

SRP par l'exemple - Résolution

							
							<?php
							Class DataImporter {
								public function __construct(
									private FileLoader $loader,
									private DataConnector $connector,
								) {}

								public function import(string $filename): void
								{
									foreach ($this->loader->load($filename) as $record) {
											$this->connector->push($record);
									}
								}
							}
							
						

Les changements de format de donnée et de chargement de la donnée ont été délégués à des abstractions. DataImporter a désormais l'unique responsabilité de faire collaborer FileLoader et DataConnector dans le but de charger dans une base de données les données contenues dans un fichier.

O comme Open-Closed Principle

Les classes d’un projet devraient être ouvertes à l’extension, mais fermées à la modification.

L’idée derrière ce principe est : ajouter de nouvelles fonctionnalités ne devrait pas casser les fonctionnalités déjà existantes.

Objectif : pouvoir enrichir les fonctionnalités d'un module sans avoir à en modifier le comportement.

OCP par l'exemple

							
							<?php
							$importer = new DataImporter(
								new CsvFileLoader(),
								new MySQLConnector(),
							);
							$importer = new DataImporter(
								new JsonFileLoader(),
								new ElasticSearchConnector()
							);
							
						

L'exemple de résolution SRP montré précédemment est totalement conforme au principe OCP. En effet, il est possible d'ajouter de nouveaux formats de sérialisation et de nouveaux connecteurs sans avoir à modifier la classe DataImporter.

L comme Liskov Substitution Principle

Les classes d’un projet devraient être ouvertes à l’extension, mais fermées à la modification.

Il doit être possible de substituer une classe "parente" par l’une de ses classes enfants (on dit aussi "dérivées"). En pratique dans la vie quotidienne, c'est comme changer un pneu crevé d'une marque X par un autre pneu d'une marque Y.

Objectif : généralise OCP à l'héritage de classes.

LSP par l'exemple

							
							<?php
							abstract class AbstractLoader implements FileLoader {
									public function load(string $filename): array {
											if (!file_exists($file)) {
													throw new \InvalidArgumentException(sprintf('%s does not exist.', $filename));
											}

											return [];
									}
							}

							class CsvFileLoader extends AbstractLoader {
									public function load(string $filename): array {
											$records = parent::load($file);
											// ...
									}
							}

							
						

CsvFileLoader hérite de la classe abstraite AbstractLoader et redéfinit sa méthode load. La signature de la méthode est respectée ainsi que les types de retour.

I comme Interface Segregation Principle

Vous ne devriez pas avoir à implémenter des méthodes dont vous n’avez pas besoin

Voyez le comme SRP appliqué aux interfaces cette fois.

Objectif : éviter d’avoir des interfaces aux multiples responsabilités et de les redécouper en multiples interfaces qui ont, elles, une seule responsabilité, quitte même à les composer.

ISP par l'exemple

							
							<?php
							interface FileLoader {
									public function load(string $filename): array;
							}

							interface FileDumper {
									public function dump(string $filename): void;
							}

							interface File implements FileLoader FileDumper {
							}
							
						

Ici on a bien séparé les interface de chargement et d'écriture de fichier dans 2 interfaces, on a même composé une nouvelle interface File qui implémente les 2 autres. Ainsi c'est à la carte.

D comme Dependency Inversion Principle

Les classes de haut niveau ne devraient pas dépendre directement des classes de bas niveau, mais d’abstractions.

Stipule qu'il faut programmer par rapport à des abstractions plutôt que des implémentations.

Objectif : découpler au maximum les dépendances et les rendre substituables. Ça ne vous parle toujours pas ? C'est normal, procédons en quelques étapes.

DIP par l'exemple - étape 1

							
							<?php
							class DataImporter
							{
									private $loader;
									private $connector;

									public function __construct()
									{
											$this->loader  = new CsvFileLoader();
											$this->gateway = new DataConnector();
									}
							}
							
						

Ici on instancie les dépendances à l'intérieur de la classe. Cela rend difficile l'extension et les tests. On dit qu'on a un couplage fort.

DIP par l'exemple - étape 2

							
							<?php
							class DataImporter
							{
									public function __construct(
										private CsvFileLoader $loader,
										private DataConnector $connector,
									) {}
							}
							
						

Ici on permet aux dépendances d'être instanciées à l'extérieur, on peut donc dire qu'elles sont injectables (tiens tiens, ne serait-ce pas le injection de DIP?).

Par contre... on dépend toujours de classes concrètes et non d'abstraction, que pouvons-nous faire ?

DIP par l'exemple - étape 3

							
							<?php
							class DataImporter
							{
									public function __construct(
										private FileLoader $loader,
										private Connector $connector,
									) {}
							}
							
						

Ici les interfaces FileLoader et Connector favorisent l'injection de n'importe quels objets implémentant ces dernières. Houra !

SOLID

Ce qu'il faut retenir

  • SOLID aide à découpler votre code et donc vous aidera à un niveau de conception et d'expérience plus avancées à le structurer en modules ainsi que de formaliser leurs moyens de collaborer.
  • Les tests sont un excellent moyen de vérifier que votre code respecte les principes SOLID.

SOLID

Pour aller plus loin

Pas de rapport direct avec SOLID... mais tout à fait dans la lignée des bonnes pratiques de développement pour écrire du code plus propre, plus lisible, les 9 règles IN-DIS-PEN-SABLES de Object Calishtenics - William Durand

Détectez

Quand une application est STUPID

STUPID ?

On vient de voir un certain nombre de bonnes pratiques avec SOLID. STUPID c'est comme le Wario de Mario, c'est exactement ce qu'il ne faut pas faire !

Quand vous relisez du code et que vous identifiez des éléments STUPID, ce sont autant d'indices (ou "code smells" en anglais) que le code est de qualité insuffisante.

Les 6 principes STUPID

  1. S comme Singleton
  2. T comme Tight Coupling
  3. U comme Untestability
  4. P comme Premature Optimization
  5. I comme Indescriptive Naming
  6. D comme Duplication

S comme Singleton

Un singleton c'est une classe instanciable une et une seule fois

              
              <?php // Design pattern fortement déconseillé
              class DatabaseConnection
              {
                  private static $instance;
                  private function __construct() {}
                  private function __clone() {}

                  public static getInstance()
                  {
                      if(is_null(self::$_instance)) {
                          self::$_instance = new DatabaseConnection();
                      }

                      return self::$_instance;
                  }
              }
              
            

T comme Tight Coupling

Si l’utilisation d’un objet nécessite la création ou l’utilisation d’un autre objet, alors ils sont dits "fortement couplés".

              
              <?php

              // Option 1 : À éviter absolument !
              class Database { ... }

              class UserReposirory {
                private Database $db;

                public function __construct()
                {
                    $this->db = new Database();
                }
              }





              // Option 2: À éviter car utilisation
              // d'une classe concrète
              class Database { ... }

              class UserReposirory {;
                public function __construct(private Database $db) {}
              }








              // Option 3: tout bon, on utilise une abstraction
              // qui permet de poser un contrat
              interface Database { ... }

              class UserReposirory {
                public function __construct(private Database $db) {}
              }
              
            

U comme Untestability

Les deux précédentes sections illustrent bien ce principe. Un couplage fort rend les tests plus difficiles (voire même impossibles) à écrire.

Une pratique courante lorsqu'on teste est de substituer une implémentation d'interface que l'on utiliserait en environnement de production par une dédiée aux tests, par exemple une implémentation qui retourne des valeurs prédéfinies.

P comme Premature Optimization

Une resource machine coûte bien moins cher qu’une journée de consulting d’un développeur. Il faut éviter de penser d’abord à l’optimisation (dans la plupart des cas) et toujours priviléger la lisibilité du code.

Enfin, quand il s'agit vraiment d'optimiser, il est plus sage de mettre en place de l'observabilité sur la plateforme où l'on fait tourner le code et de recourir à des outils de profilage, tels que blackfire par exemple.

I comme Indescriptive Naming

Évitez d'utiliser acronymes ou abréviations quand vous nommez des variables, classes ou espaces de car tout le monde n'en connaît pas forcément le sens.

Il est recommandé de passer du temps à trouver le bon nom pour un concept. Vous pouvez même sonder vos collègues ou votre manager pour trouver le nom le plus approprié ou vérifier que l'une de vos propositions leur parle.

Enfin, un nom trop long ou compliqué peut-être un très bon code smell ! En effet on se dit rapidement qu'on viole SRP.

D comme Duplication

Ce n'est pas souvent une bonne idée de dupliquer du code. Car forcément si vous devez modifier une partie de votre code, vous devez le faire à plusieurs endroits, ce qui n'est pas très SOLID.

Attention : il ne faut pas confondre duplication de code et avec réutilisation abusive de code. Par exemple, vous pouvez dans le cadre d'un project e-commerce avoir une classe Product dans un module de Catalogue produit d'une part et dans un autre module d'expédition d'autre part. Mutualiser la classe Product n'est pas forcément une bonne idée car même si elle porte le même nom, elle a des propriétés et responsabilités très distinctes selon le module.

D comme Duplication

Il existe 2 règles qui peuvent vous aider :

  1. DRY (Don't repeat yourself) : si un bloc fonctionel est dupliqué isolez-le dans une fonction/classe/trait et réutilisez-le. Les IDE ont souvent des fonctionnalités pour vous aider à extraire/remanier.
  2. KISS (Keep It Simple Stupid) : refactorisez régulièrement pour casser la complexité, et surtout... n'anticipez pas trop d'abstractions ! Bon ok dans ce cours le souci... c'est que vous allez vouloir mettre en application tout ce que l'on a vu et abstraire à tort et à raison. Vous verrez ça passera avec l'expérience ;)

Résoudre les problèmes de conception

Avec les design patterns

Design Pattern ?

  • Créationels
  • Structuraux
  • Comportementaux

Patterns Créationels

Fournissent des moyens pratiques de créer des objets

  • Singleton : à éviter le plus souvent, cf SOLID/STUPID
  • Prototype : revient très souvent à implémenter clone
  • Factory
  • Builder

Factory

On crée une classe dont la seule responsabilité est de fabriquer des classes se conformant à une interface. Exemple : Symfony LockFactory

Builder

Permet de créer un objet ou une structure d'objet par l'intermédiaire d'une classe qui va construire l'objet étape par étape. Exemple : Symfony Workflow DefinitionBuilder

Patterns Structuraux

Permettent d'assembler des objets dans des structures plus larges tout en conservant une structuration flexible et efficace.

  • Adapter : permet à des objets aux interfaces incompatibles de collaborer
  • Bridge : permet de découpler une abstraction de son implémentation afin que les deux puissent varier indépendamment
  • Composite : permet de composer des objets dans des structures et travailler sur les structures comme s'il s'agissait d'objets individuels

Patterns Structuraux

  • Decorator : permet d'attacher dynamiquement de nouvelles responsabilités à un objet existant sans le modifier et sans héritage, par composition
  • Proxy : permet de fournir un objet se substituant à un autre et de de réaliser certaines opérations autour du cycle de vie de l'objet d'origine
  • Façade : permet de fournir une interface simplifiée à un ensemble de classes complexes
  • Flyweight : permet de gérer de manière efficiente de grands ensembles d'objets

Design Pattern Adapter

							
              <?php

              // Une interface qu'on veut adapter en Notification
              interface SlackApi {
                  public function login(string $token);
                  public function sendMessage(
                    string $channel,
                    string $message,
                  );
              }

              // Notre interface de notification
              interface Notification
              {
                  public function send(string $title, string $message);
              }


              // Notre adapter
              final readonly class SlackNotification implements Notification
              {
                  public function __construct(
                    private SlackApi $slack,
                    private string $token,
                  ) {}

                  public function send(string $title, string $message)
                  {
                      $this->slack->login($this->token);
                      $this->slack->sendMessage('#general', $title.': '.$message);
                  }
              }
							
            

Bridge

Dès que l'on souhaite faire varier une abstraction en fonction de sa dépendance, on peut utiliser le Bridge. Par exemple comment rendre une même structure de données dans différent formats

Composite

Dès que l'on a une structure d'objets de type arbre, il es courant de l'utiliser pour se faciliter la vie. Par exemple comment créer le rendu d'un formulaire HTML

Decorator

							
          <?php

          // Notre interface de chargement de blob
          interface BlobLoader {
              public function load(string $blob): string;
          }

          final class FileToBlobLoader implements BlobLoader {
              public __construct(private BlobLoader $loader) {}

              public function load(string $fileName): string {
                  $content = file_get_contents($fileName);

                  return $this->loader->load($content);
              }
          }

          // Décorateur gérant la compression du fichier
          final class CompressedBlobLoader implements BlobLoader {
              public function load(string $content): string {
                  if (substr($content, 0, 2) !== 'gz') {
                      return $content;
                  }

                  return gzdecode($content);
              }
          }

          // On peut maintenant charger un fichier compressé
          // sans héritage, par composition
          $loader = new FileToBlobLoader(
            new CompressedBlobLoader()
          );
          $loader->load('file.txt');
							
            

Proxy

							
          <?php

          interface HttpClient {
              public function get(string $url): string;
          }

          final readonly class SomeApiClient implements HttpClient {
              public function __construct(private HttpClient $client) {}

              public function get(string $url): string {
                  return $this->client->get($url);
              }
          }

          final readonly class LogTimeOfApiClientProxy implements HttpClient {
              public function __construct(
                private SomeApiClient $client,
                private LoggerInterface $logger,
              ) {}

              public function get(string $url): string {
                  $start = microtime(true);
                  $response = $this->client->getSomeData();
                  $end = microtime(true);
                  $this->logger->info('Call took ' . ($end - $start) . ' seconds');

                  return $response;
              }
          }
							
            

Facade

On viole intentionnellement des principes SOLID avec la façade dans un but de simplification, pour exposer simplemnt un sous système complexe. Par exemple, Comment simplifier l'usage d'une API : télécharger une vidéo Youtube

Flyweight

Lorsque l'on a besoin d'optimiser/mutualiser des ressources en mémoire, très proche des principes de mémoization qu'on trouve en programmation foncionnelle, ici adapté à la programmation objet. Pour aller plus loin

Patterns Comportementaux

Utilisés dans les algorithmes et la répartition des responsabilités entre objets.

  • Chain of Responsibility : permet de passer un objet à une chaîne d'objets jusqu'à ce qu'il soit traité par un objet de cette chaîne
  • Command : encapsule une demande dans un objet, permettant de décider comment traiter ou annuler cette demande
  • Iterator : permet de parcourir séquentiellement une aggrégation d'objets sans avoir à exposer leur représentation
  • Mediator : définit un objet qui encapsule comment un ensemble d'objet interragissent
  • Memento : capture et externalise l'état d'un objet dans le but de le restaurer, sans violer l'encapsulation

Patterns Comportementaux

  • Observer : mécanisme d'abonnement pour notifier de multiples objets de tout événement se produisant sur un objet observé
  • State : permet à un objet d'altérer son comportement lorsque son état interne change
  • Strategy : permet de définir un ensemble d'algorithmes dans des classes distinctes et interchangeables
  • Template Method : définit le skelette d'un algorithme tout en laissant les sous classes surcharger des étapes spécifiques sans en changer la structure
  • Visitor : permet de cibler des algorithmes en fonction des objets parcourus

Chain Of Responsibility

Ce patron permet de chaîner un traitement à travers une liste de gestionnaires (handlers) : chaque handler s'il ne peut pas traiter l'objet passé transmet au handler suivant.

À l'opposé, vous rencontrerez assez souvent le concept de middleware qui passe le traitement au suivant tant qu'il évalue que l'application pourra traiter la demande.

Command

Ce patron sous sa forme historique dans le livre du gang of four consiste à avoir :

  • 1 classe de commande qui encapsule les données d'une requête à traiter
  • 1 méthode execute dans la classe de commande qui permet que la commande puisse s'éxécuter
  • En bonus, méthodes et/ou propriétés pour reporter le statut d'exécution

Command

Concept devenu plus propre avec CQS/CQRS :

  • 1 classe de commande qui encapsule les données d'une requête à traiter
  • 1 classe de gestionnaire (command handler) exécute la commande
  • Les deux classes étant reliées par un service (command bus)

Digression CQS/CQRS

Iterator

Permet simplement d'extraire la logique de parcours d'une liste ou d'une structure dans une classe dédiée.

En php et dans beaucoup de langages des interfaces sont déjà proposées pour vous guider, telles que Iterator, ou IteratorAggregate

Mediator

Le médiateur est une sorte de chef d'orchestre entre plusieurs services afin de rendre plus claire la manière dont ils s'articulent.

Un très bon exemple est la réalisation d'un event dispatcher. Très souvent des librairies/framework vous fournissent déjà des composants réutilisables reposants sur le design pattern de mediator.

Memento

Dans beaucoup de langages, le meilleur moyen de le mettre en place est d'utiliser la sérialisation/désérialisation. Elle permet de sauvegarder l'état d'un objet et de le restaurer à tout moment. On évitee de violer OCP en mettant inutilement des getters et setters sur les propriétés de vos objets.

C'est un bon moyen de mettre en place un undo/redo dans une application par exemple, en sauvegardant les états successifs de l'application (historique) et en étant donc capable de restaurer par rapport à un état précis.

Observer

Nous avons déjà vu un exemple d'Observer avec l'event dispatcher dans le pattern du mediator. Il s'agit simplement de notifier plusieurs objets (Subscribers) d'événements survenant dans notre application.

C'est un bon moyen au sein d'une application de séparer les responsabilités et de rendre plus claire la manière dont les objets s'articulent. En effet on peut facilement découpler, mettre des frontières entre des concepts différents. Et donc avec une bonne pratique, éviter l'effet spaghetti dans notre code. Un exemple d'observer sur code guru

State

À utiliser seulement quand vous avez besoin de gérer un grand nombre d'états différents dans une classe et que vous souhaitez déléguer la gestion de ces états à une autre classe.

Un bon exemple est la gestion des états d'une commande dans un système de paiement, ou encore un flux de publication d'articles (draft, reviewing, published, archived, etc.). Le composant Workflow de Symfony est un bon exemple d'implémentation de ce pattern.

Strategy

À utiliser quand vous devez mettre en place un algorithme dans des contextes différents. Par exemple de la plannification de trajet (l'algorithme) appliqué à un véhicule (contexte). Ce contexte va varier selon le véhicule, et donc l'algorithme de plannification de trajet va varier selon le véhicule.

Vous crééz donc une interface de stratégie commune à toutes les variantes de l'algorithme, et vous pouvez ainsi implémenter les algorithmes dans des classes différentes, et les passer en paramètre à votre contexte. Un exemple de stratégies de tri ou filtrage.

Template Method

Le but est d'abstraire les étapes de traitement d'un algorithme qu'on peut dériver. On fournit ainsi un cadre réutilisable pour créer des variantes.

Au minimum on a une classe abstraite qui définit les étapes de l'algorithme, et des classes concrètes qui vont implémenter. On peut même aller jusqu'à fournir quelques implémentations concrètes de base. Un exemple de template method sur code guru

À l'inverse de Strategy, Template Method repose sur l'héritage, et non sur la composition, ce qui est moins flexible en général.

Visitor

Pattern très puissant qui permet d'ajouter de nouveaux comportements sur une hiérarchie de classe sans modifier les classes elles-mêmes. Quand on parle de comportements, il vaut mieux qu'ils soient suffisamment éloignés de la logique métier de ces classes car sinon on risque de vite overengineerer notre code.

Totalement SRP et OCP compliant. Les seules limites c'est qu'il faut étendre le visiteur lorsque la hiérarchie de classe change et qu'il faut qu'il aient accès aux informations nécessaires de la hiérarchie de classe.

Par exemple, on peut imaginer un visiteur capable de produire un rapport sur les objets visités. Un exemple sur code guru

Design patterns

Ce qu'il faut retenir

  • Ne pas utiliser de design pattern pour le fun. Il faut toujours garder à l'esprit que le but est de produire du code lisible, maintenable et évolutif. l'over engineering est autant votre ennemi que le code spaghetti.
  • Bien choisir les patterns à utiliser. Il y en a beaucoup, et il faut savoir les utiliser au bon moment.
  • Plutôt que de les réimplémenter, utilisez ceux qui sont déjà disponibles dans votre framework, qui sont souvent été testés et maintenus et donc d'excellente qualité.

Ajoutez du contrôle qualité

La pyramide des tests

Anti pattern : cornet de glace

Pyramide réactualisée

  1. Analyse statique, de structure
  2. Tests unitaires
  3. Tests d'intégration et API
  4. Tests de bout en bout (e2e / End To End)
  5. Tests d'approbation
  6. Tests de charge et de sécurité

Analyse statique, de structure

Analyse statique

Détecte (parfois, corrige automatiquement)
sans éxécuter le code les erreurs de :

  • Syntaxe et mise en forme, code mort : prettier, php cs fixer, rome, pylint, ...
  • Logique : typescript, phpstan, pmd
  • Sécurité : détection des mauvaises pratiques qui conduisent à des failles (ex: XSS, injections Shell, SQL, etc.)
  • Performance : certains outils ci-dessus, sinon on utilise au runtime des APM tels que blackfire par ex.

Analyse de structure

Pour maintenir une architecture propre

Des outils tels que Deptrac.

Tests unitaires

Vérifient le bon fonctionnement d'une fonction ou d'une méthode.
Avec des outils tels que junit, phpunit, pytest, jest, ...

							<?php
use PHPUnit\Framework\TestCase;

class StackTest extends TestCase
{
    public function testPushAndPop()
    {
        $stack = [];
        $this->assertSame(0, count($stack));

        array_push($stack, 'foo');
        $this->assertSame('foo', $stack[count($stack)-1]);
        $this->assertSame(1, count($stack));
    }
}
							
            

Tests d'intégration et API

Vérifient le bon fonctionnement de vos contrats applicatifs, c'est à dire un ensemble de services répondant à une fonctionnalité utilisateur final.

On parle aussi de tests de comportement, avec des outils tels que Cucumber JVM, Behat, Cucumber JS, Behave, ...

							
              Feature: Register a vehicle
              In order to follow many vehicles with my application
              As an application user
              I should be able to register my vehicle

              Scenario: I can register a vehicle
                  Given my fleet
                  And a vehicle
                  When I register this vehicle into my fleet
                  Then this vehicle should be part of my vehicle fleet
							
            

Tests de bout en bout (e2e / End To End)

Vérifient qu'une application (web, mobile, ...) se comporte comme prévu, du début à la fin.

Avec des outils tels que Selenium, Cypress, Playwright, Appium, Detox, ...

Tests d'approbation

Ou Approval Tests, reposent essentiellement sur la comparaison de fichiers textes, via des création de snapshots. Avec des outils tels que Approval Tests, mais beaucoup d'outils de tests unitaires proposent au moins en partie cette fonctionnalité.

Particulièrement utile avant de se lancer dans des refactoring de code, encore plus lorsqu'il est legacy (code hérité).

Vous pourrez aussi entendre, Golden Master, Snaphot Tests, Locking Tests, Regression Tests, ... pour parler de ces tests.

Tests de charge et de sécurité

Le test de charge (load testing) consiste à mesure la performance d'une application, le plus souvent en mesurant les temps de réponse en fonction de la sollicitaton. Avec des outils tels que : JMeter, Gatling, k6, Locust,

Le test de sécurité ou pentest (penetration testing) consiste à tester les failles de sécurité d'une application, le plus souvent en simulant des attaques. Un dépôt Github pour se sensibiliser sur le sujet, ouils et techniques.

Pyramide de tests

Ce qu'il faut retenir

  • Les tests unitaires sont les plus rapides à écrire et à maintenir
  • Essayez d'écrire les tests d'abord, puis le code, pour vous aider à mieux concevoir
  • Essayez de ne pas tester deux fois la même fonctionnalité avec des des types de tests différents ou dans dans des tests d'intégration distincts.

Pour aller plus loin

  • Assurez-vous d'avoir les extensions de votre éditeur de code qui vous aident à formatter votre code, détecter des erreurs et tester facilement.
  • Mettez en place une intégration continue : Github Actions, Jenkins, ...
  • Perfectionnez-vous en BDD (Behavioral Driven Development) avec cette vidéo.

Rendez votre documentation vivante

Rendez votre documentation vivante

La documentation la plus à jour, c'est le code.

... mais vous ne pouvez pas exiger d'une équipe tierce (interne ou externe) de lire le code pour comprendre comment utiliser votre API par exemple.

Documentez vos contrats

Ce qui décrit les règles de communication entre deux parties.

Les contrats peuvent être :

  • Des scénarios d'utilisation (cucumber/gherkin)
  • Des API (GraphQL, REST, RPC, ...)
  • Des librairies développées pour aider des tiers à s'intégrer/étendre les fonctionnalités de votre application
  • Des composants d'interface graphique utilisateur réutilisables

Documentation d'API

Swagger est un standard de documentation d'API, en particulier OpenAPI.

Avec Symfony et API Platform, vous pouvez générer facilement un fichier OpenAPI à partir de vos annotations de code et donc publier une documentation d'API toujours à jour si vous enrichissez votre pipeline de CI/CD.

Documentation d'API Événementielle

Lorsque vous développez une application basée sur des événements (traitements asynchrones), vous pouvez générer une documentation au format AsyncAPI.

EventCatalog est un outil qui permet de présenter cette documentation, et plus.

Documentation de composants graphiques

Des outils tels que StoryBook ou Backlight permettent de documenter des composants graphiques.

Documentation de décisions

Se reposer sur le principe d'ADR (Architecture Decision Records) pour documenter les décisions prises et avoir un historique.

Les commentaires dans le code

Si le code est clean, les commentaires sont pratiquement inutiles

  • Pas de paraphase : documenter le type d'une variable d'un langage typé
  • Le strict nécessaire : comme ce n'est pas du code, ils deviennet facilement obsolètes.

Documentation Vivante

Pour aller plus loin

Living Documentation : vous allez aimer la documentation ! (Cyrille Martraire)