Suggestions sur le développement d'applications

Mathias, 2016-08 > 2017-08.
En vrac, incomplet, n'est que mon humble avis.

Écrire une fonction

commencer par traiter les cas d'erreur

Afin de ne pas exécuter du code lorsqu'on a déjà les éléments nécessaires pour savoir qu'on est dans un cas d'erreur, il est préférable de traiter ceux-ci en premier et sortir de la fonction avec un return ou en jetant une exception portant un message clair. Une fois traités ces cas, on est sûr que toutes les conditions sont réunies pour exécuter le code correspondant au cas général.

Exemple (extrait de Cumulus) :
public function getByDate($dateColumn, $date1, $date2, $operator="=") {
	if (empty($date1)) {
		throw new Exception('storage: no date specified');
	}
	if (!in_array($operator, array("=", "<", ">"))) {
		throw new Exception('storage: operator must be < or > or =');
	}
	if (! in_array($dateColumn, array(self::COLUMN_CREATION_DATE, self::COLUMN_LAST_MODIFICATION_DATE))) {
		throw new Exception('storage: column must be ' . self::COLUMN_CREATION_DATE . ' or ' . self::COLUMN_LAST_MODIFICATION_DATE);
	}

	// cas d'erreur éliminés, on peut traiter le cas général tranquillement !
	// ...
}

écrire des docblocks

Chaque fonction devrait être assortie d'un docblock, qui explique :
  • à quoi sert la fonction et comment elle marche
  • comment sont utilisés ses paramètres
  • ce qu'elle retourne et dans quel cas
  • les éventuels pièges à connaître
  • les éventuelles références

Exemple (extrait de l'Annuaire) :
/**
 * Retourne true si l'utilisateur ayant l'adresse courriel $courriel a un
 * mot de passe égal à $mdp, false sinon
 * 
 * Gère également la connexion par nom d'utilisateur : si $courriel ne
 * contient pas le caractère "@", tentera de trouver l'utilisateur ayant
 * pour "user_login" $courriel
 * 
 * Compatible avec les mots de passe hachés en MD5 (ancienne méthode) ou
 * avec PHPass (nouvelle méthode); met à jour le haché si besoin :
 * https://developer.wordpress.org/reference/functions/wp_check_password/
 * 
 */
public function identificationCourrielMdp($courriel, $mdp) {
(...)

faire le moins possible de return

Ça peut sembler contradictoire avec le point "commencer par traiter les cas d'erreur"…
Une fonction ne devrait avoir qu'un return à la fin, sauf dans les cas suivants :
  • cas d'erreur du début
  • besoin impérieux de sortir de la fonction sans quoi la suite du code va provoquer des catastrophes
Oui mais : ces deux cas peuvent et devraient, si le flot d'exécution est compatible (càd. que la fonction appelante sait comment réagir) être remplacés par des jets d'Exceptions !

utiliser des variables intermédiaires

En les nommant correctement, des variables intermédiaires aident à comprendre le code, et le rendent plus simple à réutiliser (évitent de faire deux fois les calculs implicites).

Exemple (extrait de l'Annuaire) : plutôt que d'écrire
return ($passwordHasher->CheckPassword($mdp, $d['user_pass']) || (md5($mdp) == $d['user_pass']));

je préfère écrire :
$mdpHache = $d['user_pass'];
$passwordMatch = $passwordHasher->CheckPassword($mdp, $mdpHache);
$correspondanceMD5 = (md5($mdp) == $mdpHache);

return ($passwordMatch || $correspondanceMD5);


Choisir des technologies

À moins d'avoir besoin de performances militaires ou d'appels système, ne jamais utiliser de langages compilés.
N'utiliser que des technologies libres, standard, éprouvées.
Dans un contexte de production (qui finit toujours par se produire), attendre au moins 2 ans avant de choisir une technologie émergente.

Architecturer une application

  • faire 3 couches : bibliothèque (métier), webservices et interface.
  • ne séparer la couche de stockage de la couche métier qu'en cas de besoin précis.
  • la couche métier (en tout cas sa partie haut niveau) et la couche de webservices doivent implémenter la même interface (au sens POO).
  • cette interface doit contenir toutes les méthodes dites de haut niveau, qui correspondent aux cas d'utilisation de l'application.
  • les méthodes de plus bas niveau ("remplissage de trous", voir chapitre "Écrire un algorithme") doivent être implémentées dans la couche métier, sans manière de faire particulière.
  • le module d'authentification doit toujours être remplaçable (pattern "adapter" et interface, par exemple).
  • utiliser des bibliothèques existantes partout où c'est possible (Composer / Bower / npm etc.), à condition que la bibliothèque fasse une chose et la fasse bien, et ne pèse pas 40 tonnes.

Écrire des commentaires

Un commentaire ne doit pas décrire ce qui se passe (à moins que ça ne soit pas évident), mais expliquer pourquoi cela se passe (c'est à dire apporter de l'information).
Autres utilisations : signaler les pièges, signaler une incompréhension, noter quelque chose à faire plus tard.

exemple de commentaire poucrave
// On prend la date courante
$date_courante = date("Y-m-d H:i:s");

exemple de commentaire potable
(extrait de l'Annuaire)
// insertion, avec déclenchement des hooks !
// l'id est toujours retourné, qu'il soit nouveau ou = au $id précédent
$id = wp_insert_user($donnees);


Écrire un algorithme

  • définir une stratégie logique de haut niveau
  • l'écrire sous une forme la plus courte possible ne contenant (quasiment) que des structures logiques et des appels de fonctions
  • "remplir les trous" en écrivant le code des fonctions de haut niveau, de la même manière : la logique d'abord, des sous-fonctions si besoin, le code lorsqu'il est suffisamment simple à écrire et qu'on est sûr de lui faire faire une chose et la faire bien. Bon c'est pas très bien expliqué mais on se comprend.

Utiliser un framework (?)

La majorité des applications qui nous concernent n'ont pas du tout besoin d'un framework pour fonctionner.
Pour ne pas réinventer la roue, utiliser plutôt des bibliothèques séparées dédiées à chaque tâche.
N'utiliser un framework que pour de grosses applis, avec beaucoup de mécanismes génériques.
Pour ne pas avoir à maintenir un framework, ce qui gâche en grande partie le temps économisé à en utiliser un, prendre un framework déjà existant, il y a des gens dont c'est le métier qui font ça très bien.

Concevoir des applications Javascript monopage

assurer une bijection entre l'URL et l'état de l'appli

L'URL devrait toujours refléter l'état de l'application à travers son fragment (partie de l'URL après le #), et l'application devrait toujours être capable de charger son état en fonction de l'URL avec laquelle elle a été appelée.
Ainsi :
  • quel que soit l'état de l'application, un copier-coller de l'URL permet de partager cet état et
  • les écouteurs de clic sur les liens/boutons qui font changer l'état de l'appli peuvent être remplacés par des liens standard pointant sur une URL reflétant l'état désiré (plus facile, plus standard, moins de code)

Ce principe est appliqué (partiellement car comme un con j'ai fait l'appli en 2 pages au lieu d'une seule) dans ezmlm-forum.

une action sur un événement = une fonction

Toute action déclenchée lors d'un changement d'état / de valeur d'un élément (valeur saisie, case cochée…) devrait faire l'objet d'une fonction séparée.
Cela permet entre autres d'exécuter ces fonctions une fois au chargement de la page, afin d'être sûr que l'interface reflète l'état des données.

Ex : griser / faire apparaître / disparaître des éléments lorsqu'une case a été cochée.

toujours mettre un curseur d'attente lors d'une opération asynchrone

Même si elle est en général très rapide, on n'est jamais certain du temps qu'une opération asynchrone va mettre pour se terminer, ni si elle va se terminer.
Pour ne pas laisser croire qu'une opération a fonctionné si ce n'est pas le cas, afficher systématiquement un curseur d'attente : dans le cas général on ne le verra pas (opération quasi-instantanée) et en cas d'erreur on sera averti visuellement du problème (le curseur ne disparaîtra pas).

Conventions de codage

On s'en fout. Ce n'est pas parce qu'une ligne est indentée avec des espaces ou des tabulations que le code fonctionne mieux ou moins bien.

Essayer tout de même d'avoir un minimum de bon sens :
  • indenter les blocs
  • laisser des espaces autour des opérateurs (plus lisible - goût personnel)
  • donner des noms porteurs d'un peu de sens aux variables (le moins possible de $x, $pft ni $i2)
  • ne pas faire des lignes de 400 caractères de long (chiant à lire dans l'éditeur)
  • ne pas chaîner 10 instructions sur la même ligne (difficile à lire et à débugger)
  • ...

Formats de données

Pour l'échange (webservices) et le stockage de données structurées, sauf exception accompagnée d'une bonne raison, utiliser JSON.
Défaut de JSON : ne gère pas les commentaires
Proposition de solution : utiliser des clefs commençant par "_" pour signaler un commentaire.

Pour le stockage de texte, utiliser Markdown ou du texte brut.

Ne jamais utiliser XML pour quoi que ce soit sauf si un partenaire l'exige.

Concevoir des Webservices

Une URL invalide doit toujours donner lieu à une erreur HTTP 4xx
Une erreur dans la couche métier doit toujours donner lieu à une erreur HTTP 5xx
La seule façon d'identifier si un appel de service a réussi ou non est d'examiner le code de retour HTTP, il ne faut jamais se baser sur quoi que ce soit d'autre (message, code interne etc.).
Le retour d'erreurs doit être uniforme : un code HTTP 4xx ou 5xx, et un message détaillant l'erreur; pour plus d'extensibilité, encapsuler le message dans un objet, sous une clef définie et qui ne change jamais.
Utiliser un schéma REST correct
Chaque fois que la couche de services appelle la couche métier, encapsuler cet appel par un try, et dans le catch transformer le message de l'Exception en erreur 5xx.

La gestion d'erreurs

Si le langage gère les Exceptions (par exemple PHP), utiliser ça et uniquement ça.

Ne surtout pas essayer comme Papyrus ou TB Framework de :
  • gérer le niveau de rapport d'erreurs dans l'appli
  • attraper les erreurs ou les exceptions systématiquement
Les langages savent faire ça et le font bien. À part casser les standards, tout péter et qu'on comprenne plus rien, ça ne sert à rien de changer ça dans l'appli.

Tester ses applications

:-)
"Un vrai test, c'est un test en prod"
(auteur : un con moi)

Les interfaces Web

Une interface doit toujours refléter l'état du serveur.
Corollaire : une interface ne doit jamais "s'avancer" et afficher des informations sans avoir vérifié que le serveur les a bien enregistrées (ex: vote sur Identiplante).
Bonne pratique : envoyer un ordre au serveur et recharger la portion de vue affectée.

En vrac

Un calcul de données élaborées ne doit jamais être fait à plus d'un endroit du code, même si le code est séparé en plusieurs couches (interface / service).

…d'autres trucs ?