20 ans d'XP, de développeur à CTO en passant par responsable
qualité logicielle.
Qui êtes-vous ?
⏳ 5 min chit-chat
Introduction
Pourquoi
La qualité logicielle ?
3 principaux enjeux*
Stratégique
Économique
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.
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 :
Facile à faire évoluer, on comprend qui est responsable de quoi
Organisé en modules découplés
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 :
Favorise toujours l'explicite à l'implicite
Suit des règles de conception
Est documenté par ses cas d'usage
À la fin de ce module :
Les règles élémentaires de la programmation fonctionnelle
La création d'un objet et de ses dépendances
La gestion de la relation entre objets
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.
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é)
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.
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é.
Quand vous écrivez du code pour la 1ère fois, faites le
simplement.
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.
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".
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.
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
S comme
Single Responsibility Principle
O comme Open-Closed Principle
L comme
Liskov Substitution Principle
I comme
Interface Segregation Principle
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
S comme Singleton
T comme Tight Coupling
U comme Untestability
P comme Premature Optimization
I comme Indescriptive Naming
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 :
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.
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);
}
}
<?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;
}
}
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)
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
Analyse statique, de structure
Tests unitaires
Tests d'intégration et API
Tests de bout en bout (e2e / End To End)
Tests d'approbation
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 :
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.
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.
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.