Coder de façon satisfaisante.


97 Things Every Programmer Should Know
Bonjour tout le monde ! Voici le second article qui parle des bonnes pratiques de programmation que je traduis et met en forme du livre “97 things every programmer should know”. Je vous conseille la lecture du numéro premier avant celui-ci si vous ne l’avez pas encore fait, bien que les aspects couverts ne sont pas dépendants.

Comme avant, ces avis ne viennent pas de moi, et j’ai essayé de reporter les dires du livre avec la plus grande des objectivités, même si ça ne fonctionne probablement pas toujours très bien. :p

~~~~~
La mentalité du bon développeur.
Coder de façon satisfaisante.
Moi, et les autres. (Sera disponible un jour)
~~~~~

Sommaire :

Nous avons vu dans un premier temps quel état d’esprit était le plus favorable à la réussite dans le monde de la création logicielle et collaborative. Oui, même en étant auteur de quelque chose de propriétaire, on fait partie d’une collaboration de gens travaillant ensemble 🙂 . Ces caractéristiques pouvaient même s’étendre sur des sujets bien plus vastes, comme l’administration réseau, ou d’autres secteurs totalement différents de ceux en rapport avec l’informatique. Nous allons à présent plus nous axer sur les façons de produire du code source. Ainsi, la sélection des domaines couverts se retrouve maintenant plus petite.

code_xana

Écrire un code fragmenté.

Produire un programme consiste essentiellement dans l’élaboration de son code source. Comment faire, pour l’écrire correctement ? Une bonne façon de procéder serait déjà de s’inspirer de celui des autres. Clonez le dépôt d’un projet libre qui vous plait et regardez comment les développeurs s’y sont pris. En effet, évoluer avec des dizaines, des centaines, voir même des milliers de personnes différentes demande une structure et des conventions très claires pour éviter que tout se casse la gueule. Les adopter comme modèle ne peut être que bénéfique pour vous, car ceux-ci auront réussi à éprouver une charge considérable, tant grâce à la taille des fichiers sources que par le nombre de contributeurs capable de travailler simultanément dessus. C’est un gros gage de qualité.

Utiliser un code atomisé participe également à la robustesse du projet : toutes les fonctions doivent faire une seule tâche. Elle doit aussi être unique afin de respecter l’encapsulation. Non seulement ça améliore grandement sa lisibilité, mais en plus permet de rapidement et efficacement changer l’implémentation de celle-ci. Le fait d’avoir une entité se chargeant d’une action simple nous autorise à donner des noms clairs aux choses. En effet, on ne devrait pas avoir besoin de plonger dans le code pour comprendre ce qu’il s’y passe. Par extension, il est aussi nécessaire de déclarer peu de variables. Ça nous forcera à naturellement réduire les tâches, améliorant ainsi les performances, et encore une fois, la lisibilité ainsi que la robustesse dans les gros projets.

De la même façon, la communication entre les différentes fonctions, ou objets devrait être du plus étroit des moyens possibles. Pour cette raison, il est déconseillé d’employer plus de quatre paramètres. Si tel est le cas, la définition n’est probablement pas assez haut niveau et la mise en pratique de types utilisateurs est préférable. Par exemple, display_logs_in_color() qui affiche des informations du registre ne devrait en contenir que deux : le log, et la couleur. Ça sera à la variable « log » de posséder les différentes données dont elle dépend, comme l’heure, le genre d’événements, et finalement le message. Elles ne devrait en aucun cas être visible dans le prototype de la fonction.

Enfin, écrire une ligne de code demande bien plus d’efforts qu’il n’y paraît au premier abord. Il faut la maintenir, la documenter et la tester. De plus, ça n’alourdit pas que le développeur l’utilisant, mais aussi l’ordinateur qui aura à la compiler et à l’exécuter… C’est pour cette raison qu’il est vivement conseillé de n’implémenter que ce qui est réellement nécessaire sur le coup ! Ne perdons pas de temps avec quelque chose « peut-être utiles dans le futur ». De cette façon, on ne pollue pas le code de fonctions, voire d’objets sans intérêts pouvant demander une charge de travail supplémentaire conséquente.

Écrire un code lisible.

pieds

Par lisible, j’entends quelque chose qu’on peut comprendre sans avoir à se repêcher sur toute la structure du programme et le processus d’implémentation adopté. Même les solutions très complexes (surtout elles, en fait…) doivent être rédigées de façons simples. Cela permettra de prendre conscience des subtilités mises en œuvre pour répondre à la problématique, sans que ceux-ci ne soient offusqués dans un code mal foutu.
Celui-ci devrait être facile à survoler : les patterns doivent être visuels en respectant, par exemple, de bonnes techniques d’indentation accordant une valeur particulière aux informations les plus importantes. En effet, si deux fonctions ont une apparence similaire, il est aisé d’apercevoir les différences. Une autre grosse limite est le nombre de pixels dont disposent nos écrans. Éviter de briser les contextes en ayant l’affichage d’une fonction intégralement sans devoir scroller aide grandement. Ainsi, il faut employer des designs compacts, et proscrire les sauts de lignes abusifs.

Une des techniques les plus efficaces est d’écrire un code DRY (don’t repeat yourself), soit, avec le moins de répétitions possible. N’oubliez pas que chaque instruction doit être maintenue, et comprise par les collègues. Donc moins il y en a, mieux c’est. Le second problème est qu’il peut vite devenir très fastidieux de procéder à la moindre modification, car celle-ci devra avoir lieu à plusieurs endroits… En fait, une bonne partie des designs patterns ont pour objectif de limiter ces répétitions : chaque élément doit avoir un unique, clair et exclusif but dans un même programme.
Enfin, quelque chose de DRY a également l’avantage d’être plus simple à tester automatiquement, car les tests unitaires n’ont généralement qu’à être effectués à une seule place pour savoir si l’action est fonctionnelle ou pas.

Nous pouvons aussi toucher quelques mots sur les commentaires, souvent utilisés à tort. Ceux-ci s’avèrent en général nécessaires, mais pas en paraphrasant la source. C’est-à-dire que quelque chose du genre : « On effectue la moyenne des entrées de l’utilisateur » ne fera qu’alourdir, car on recopiera ce que le code nous montre déjà. Ils devraient plutôt être employés pour générer automatiquement les documentations de nos classes à l’aide de certains outils, ou pour mettre en évidence les décisions prises dans des cas vraiment subtils. Enfin, ils sont aussi appropriés quand le manque de clarté provient d’un élément extérieur (introduire brièvement les signaux « kill » d’Unix dans une fonction interagissant avec, par exemple).
En fait, la règle de base des commentaires consiste à les utiliser uniquement pour expliquer ce que le code ne peut pas dire, pas ce qu’il obfusque.

Pratiques d’implémentations orientées objet.

Après nous être attardé sur ce qui rend un code lisible, on va se pencher sur quelques astuces nous permettant de correctement aborder le paradigme objet. En effet, il s’agit d’une méthode d’encapsulation assez complexe. Nous avons les bibliothèques qui sont de gros packages, les fonctions qui sont bien plus atomiques, et enfin les classes, qui elles se situent au milieu. Nous pouvons considérer ces derniers comme étant des structures mémorisant des données, mais aussi des comportements.

cubes

Une classe se doit d’être simple, mais pas « bête ». Par exemple, « Porte » pourrait avoir une variable d’état pouvant obtenir les valeurs « ouvert », « en cours d’ouverture », « en cours de fermeture », « fermé », et également deux fonctions « ouvrir » et « fermer », qui n’agiront pas forcément de façon similaire dans tous les cas de figure. Cette implémentation dynamique sera définie dans le corps même de la classe. Ainsi, un utilisateur externe ne devrait jamais avoir à employer des itérateurs pour modifier l’état de la porte, mais passer uniquement par les méthodes dédiées à cet effet. Prenons l’exemple d’un objet « Humain », voulant l’ouvrir. L’appel devrait se faire comme ça : porte.ouvrir(), et non pas quelque chose du genre : porte.set(porte.OUVERT) (en considérant OUVERT comme étant une variable statique, dont la valeur correspond à l’état d’ouverture). De cette façon, « porte » restera toujours maître des actions à effectuer et des changements à apporter. C’est le principe de base de l’encapsulation objet : mettre l’implémentation de toutes les tâches de l’objet dans celui-ci, et pas dans la structure parente. Un objet contient ses états ET ses comportements.

À l’inverse, la deuxième mauvaise tendance qu’ont les développeurs vis-à-vis des classes est de leur donner trop de buts. En effet, chacune d’elles ne devrait avoir qu’une seule raison d’être modifiée, car cela permettra à notre application d’avoir une certaine stabilité. Il ne faut effectivement pas oublier qu’à chaque réécriture, même partielle, c’est indirectement tous les codes qui l’appelle que l’on modifie aussi, et qui donc, devront être recompilées et redéployées.
On peut prendre l’exemple de cette mauvaise implémentation d’une classe « Employee » qui paraît pourtant logique :

public class Employee {
	public Money calculatePay() ;
	public String reportHours() ;
	public void save() ;
}

La fonction « calculatePay » changera dès que les taux horaires, ou règles de calcul ne serons plus les mêmes, « reportHours » aura des problèmes le jour ou la syntaxe des logs sera redéfinie et « save » doit être réimplémenté à la moindre nouveauté dans la base de données. Notre classe est donc fortement volatile, ce qui l’amènera à être souvent modifiée… Voici une implémentation respectant le principe de la responsabilité unique :

public class Employee {
	public Money calculatePay() ;
}
public class EmployeeReporter {
	public String reportHours(Employee e) ;
}
public class EmployeeRepository {
	public void save(Employee e) ;
}

Je profite aussi de l’exemple ci-dessus pour parler des types utilisateurs. Nous voyons que nos classes « EmployeeReporter » et « EmployeeRepository » exigent un paramètre de type « Employee ». À première vue cela semble évident, mais en pratique, nombre de développeurs se seraient contenté d’un entier correspondant à l’id de l’employé dans la base de données. C’est après tout cette information qui sera nécessaire, pourquoi donc se prendre la peine de transmettre tout un objet ? Premièrement, parce que ça améliore grandement la lisibilité, et ensuite, car ça nous autorise à faire des tests sur la validité des données.
Voyons l’exemple d’une fonction « pressionAtmosphérique ». Si son prototype est de la forme pressionAtmosphérique(Kilometre altitude), on sait très exactement ce qui est demandé, et permet très clairement de comprendre la différence avec pressionAtmosphérique(Pied altitude), ce qui n’aurait pas été le cas avec pressionAtmosphérique(double altitude). Un problème du genre a coûté 327 millions de dollars à la NASA en 1999 [1].

Un autre système d’amélioration de la lisibilité et de la fiabilité d’un programme à l’aide du paradigme objet est d’utiliser le polymorphisme. Il peut être utilisé à la place de structures conditionnelles statiques, comme switch dans le cas d’exécution d’une procédure dépendant de paramètres d’un type d’objet spécifique.

Pour illustrer cette phrase un peu complexe, prenons l’exemple d’un script de « no-ip » que j’ai rédigé récemment. Comme son titre l’indique, celui-ci a pour objectif de mettre à jour automatiquement un nom de domaine en fonction de l’ip dynamique allouée à l’hôte. L’ennui, c’est que les registars, ne fournissent pas tous la même API. Dans le souci de faire un code adaptable, j’ai créé plusieurs classes pour tous ceux susceptibles d’être utilisés. Comment donc faire comprendre à notre objet d’employer les procédures provenant de « GandiManager », ou « OVHManager » ? Soit avec une structure conditionnelle classique demandant d’être mise à jour à chaque modification des registars supportés :

class MiseAJourDomaine {
	…
	void miseAJour() {
		if (typeManager == “OVH”)
			OVHManager.maj(nouvelleIP) ;
		else if (typeManager == “Gandi”)
			GandiManager.maj(nouvelleIP) ;
	… }
… }

Soit en utilisant la notion de polymorphisme, nous autorisant à créer des classes virtuelles dynamiquement :

class MiseAJourDomaine {
	…
	Manager.maj(nouvelleIP) ;
…}

Avec « Manager.maj » pointant sur une fonction virtuelle des classes filles de « Manager » (« OVHManager » ou « GandiManager »). Ainsi, on évite à « MiseAJourDomaine » de se tenir informé des nouveaux régistars disponibles, car le bon sera appelé dynamiquement au moment de l’exécution du programme. C’est quelque chose d’un peu compliqué à comprendre. Si vous n’avez jamais entendu parler de polymorphisme avant, je vous conseille vivement d’apprendre en quoi ça consiste concrètement et de ne pas rester sur mon explication incomplète et confuse.

Pratique de maintenance.

Eh oui, c’est super complexe de développer quelque chose de fonctionnel, et encore plus si l’on souhaite faire les choses proprement. Rien qu’à la lecture de cet article, le total néophile devrait se rendre compte de la difficulté de la tâche, notamment quand on bosse sur un très gros projet… Comment faire pour éviter de mourir perdu sous son code, ou sous la charge de travail ? C’est cette question qu’on va tenter d’aborder dans cette prochaine partie.

Évidemment, la première chose à faire reste d’être à l’écoute de son programme. Si celui-ci émet cinq warnings par minutes dans les logs, c’est qu’il y a un problème quelque part. On peut les ignorer, mais ce n’est pas comme ça que l’on construira quelque chose de fiable. Il faut garder à l’esprit que même ces anomalies sont des erreurs de traitement au niveau de l’algorithme, les enchaîner n’est jamais très bon. Il en est de même concernant le test de la valeur de retour d’un script, ou des exceptions. Si ces pratiques existent, c’est qu’elles servent à quelque chose. Trop souvent, nous voyons des codes ressemblant à ceci :

try {
	…
} catch (…) {} // On ignore l'exception.

C’est mal. Nous devons toujours être en mesure de savoir si quelque chose c’est pas bien déroulé pour résoudre le problème, ou au moins pour avoir conscience de la faiblesse.

On peut faire la même remarque pour les messages d’erreurs vis-à-vis de l’utilisateur. Combien de fois avez-vous eu un comportement non attendu sans avoir aucune explication de ce qui s’est passé ? Avec de la chance, vous avez droit à un popup du genre « Une erreur c’est produite. », histoire d’être au courant qu’il y a quelque chose qui cloche, mais jamais d’informations concrètes. Cela provient d’une étrange manie de prendre les gens pour des cons en leur cachant la moindre once de complexité pour éviter de « détériorer » l’expérience utilisateur. Pourtant, le fait de leur indiquer clairement ce qui ne va pas aura non seulement une portée didactique vis-à-vis du logiciel (en apprenant comment il fonctionne), mais peut aussi permettre de décharger l’équipe d’assistance. L’utilisateur habile pourra en effet, se débrouiller tout seul, ou avec l’aide de la communauté pour fixer la panne.

bob

Le mieux étant de réussir à éviter les erreurs, les conseils suivants parleront de la qualité du code fréquemment négligée au détriment de la durée d’implémentation. En effet, nous avons toujours du retard. Pour respecter les timelines nous avons donc à faire des sacrifices, et souvent nous nous autorisons à rendre un programme loin d’être optimal dans l’optique de finir « plus tard » les choses correctement. Seulement voilà, les jours passent et le travail demandé s’enchaîne, laissant le plus souvent ces dettes techniques à l’abandon. Mais un jour, elles nous pètent à la gueule entraînant une perte de temps énorme pour la mise en conformité de tout ce qui a été réalisé depuis dont la fiabilité dépend. Il est donc essentiel de payer ces dettes le plus tôt possible, car les intérêts montent vite. Penser pouvoir résoudre tous ces écarts sur le coup est illusoire, et vous devriez prévoir un créneau dans votre projet spécialement pour cette cause. En garder une trace (avec une catégorie spécialement consacrée sur le bug tracker, par exemple) permet de prendre l’ampleur de la chose et de définir quand il devient nécessaire de s’en occuper.

Dans un état d’esprit similaire, il ne faudrait pas laisser notre source se détériorer. Dans le même sens que nos devoirs civiques nous dictent à quel point il est mal de balancer des déchets dans la nature, celui de développeur devrait nous interdire de délibérément accepter quelque chose de non optimal. On devrait donc se forcer d’améliorer chaque ligne de code que nous parcourons, si le besoin s’en fait sentir. Cela peut aller du nom de la variable pas clair à la segmentation d’une grosse fonction en d’autres plus petites. Ce travail de relecture indirecte et constante devrait permettre à notre programme d’éviter les « zones obscures » dont plus personne n’y comprend rien.

Un autre piège dans lequel il ne faut pas tomber se trouve dans l’utilisation des bibliothèques externes. Effectivement, il nous est de plus en plus demandé de faire des créations très complètes et le plus rapidement possible. Il est donc nécessaire pour nous de nous tourner vers des frameworks de développements spécialisés. Cela fait généralement gagner du temps, et par extension de l’argent. En effet, même si le module employé est payant, le prix de l’achat est fréquemment inférieur à celui de l’écriture et à la maintenance de la fonctionnalité par nous même. Le problème dans tout ça, c’est que nombres de programmes aujourd’hui ne sont plus que de gros mélanges de multitudes de modules, et dont notre seule véritable tâche consiste à les configurer et les hacker pour qu’ils puissent interopérer ensemble.
Comme vous le voyez c’est assez ignoble, ne serait-ce parce que chaque module est développé bien souvent avec des normes de codage différentes. À chaque mise à jour de ces modules, il sera demandé au mainteneur de notre logiciel d’intégrer cette nouvelle version, ce qui encore une fois peut s’avérer très fastidieux si des hacks sont obligatoires pour le faire fonctionner correctement. Le conseil ici est donc d’essayer de se passer le plus possible de ces bibliothèques externes et de nous contenter du strict minimum. Si l’utilisation de certaines reste nécessaire, il faut écrire des interfaces entre l’API disponible et notre programme, et non directement les fusionner à l’intérieur de celui-ci. Comme ça, seules celles-ci devront être modifiées si l’API change, et les normes de codage de celle-ci ne viendront pas interférer avec ceux de notre logiciel.

Enfin, pour éviter d’introduire des bugs, il est conseillé de pratiquer le principe du moindre privilège en créant les structures de telle sorte qu’elles aient accès uniquement à ce qu’il est obligatoire d’avoir. Non seulement cela permet de réduire le nombre d’erreurs car aucune situation non gérée n’est laissée exécutable, mais en plus ça peut souvent aider dans l’allègement du programme. En effet, ces fonctions « bridée » demandent logiquement moins de ressources.
Nous pouvons prendre l’exemple d’un test sur une valeur pouvant aller de 0 à 5 et retournant « vrai » si elle est inférieure à 2 :

boolean fonction(int valeur)
	return valeur < 2 ;

En fait là, on ne teste pas le contenu d'une variable pouvant être comprise entre six états, mais pouvant l'être entre 8 589 934 592 (toutes les valeurs possibles pour un type « int » en Java). Imaginez faire le test pour toutes les cas, comme ça devrait l'être pour certifier que nons avons bien le résultat attendu… ^^
Cette seconde solution, en plus d'ajouter de la lisibilité et potentiellement de l'optimalité à la fonction (cela dépend du compilateur qui n'attribuera pas forcément une variable integer à une énumération ne pouvant avoir que six états), permettra d'avoir un champ d'action beaucoup plus restreint et facile à tester.

enum Val = {0, 1, 2, 3, 4, 5}
boolean fonction(Val valeur)
	return valeur < 2 ;

Certes, cet exemple est trivial. Il est difficile de penser que le résultat pourrait redevenir vrai après 2… Mais remplacez l'opérateur d'infériorité par celui de comparaison avec différents éléments d'un tableau chargée dynamiquement, et le concept prendra déjà sens.

Penser aux bonnes choses.

Mind_ReInstallation

Comme pour les maisons, la chose la plus importante dans la création logicielle pour réaliser quelque chose de robuste se trouve dans la construction des fondations. Or, celles-ci représentent essentiellement l'aptitude du développeur à raisonner le plus adéquatement possible. Pour ce faire, nous allons introduire dans cette partie quelques points intéressants, et souvent sous-estimés à prendre en compte dans l'élaboration de notre projet.

Comme je l'avais déjà dit dans l'article précédent de cette série, avoir conscience de l'existence de différents paradigmes est une très bonne façon d'opter pour différents points de vue quand nous tentons d'évaluer la meilleure méthode d'implémentation à mettre en œuvre. De nos jours, la programmation fonctionnelle avec une architecture en flot de données est, par exemple, de plus en plus utilisée, car elle est l'une des plus efficaces sur un environnement multi-threadé et résous le problème de l'accès à la mémoire partagée. En effet, il ne faut surtout pas oublier d'adapter notre logiciel à la machine sur laquelle il est sensé tourner : serait-ce sur un système 64 bits ? Avec beaucoup de RAM ? Quel type de processeur ? Ces éléments peuvent très fortement influencer sur la façon de concevoir certaines solutions à haute performance. Pensez donc à inclure la dimension physique dans votre étude de cas. Se contenter de la syntaxe du langage et de l'IDE sur lequel on souhaite travailler ne représente pas des critères suffisants. 😉

À la place de fragmenter l'exécution du programme sur différents processeurs, certaines équipes de développement choisissent un découpage plus fonctionnel, à l'aide d'IPC (interprocess communication). C'est par exemple le cas lorsque nous déléguons au système de gestion de la base de données la récupération d'une info, soit quand on utilise des processus communs appelés par les autres dans un souci d'optimisation. La aussi, nous avons trop tendance à rechercher la subtilité dans l'algorithme qui pourrait rendre le script plus rapide de quelques millisecondes, alors qu'il est souvent bien plus efficace d'améliorer les relations entre IPCs. Cela peut être effectué en limitant leurs nombres, en situant tous ces services sur des ordinateurs très proches dans le réseau (liaison à très faible latence, et haute bande passante), en ne transmettant que les données réellement nécessaires et pas de superflu (un SELECT * FROM … quand seulement deux champs de la table nous intéressent), ou encore en mettant en cache pour ne pas redemander la même information.

En parlant de base de données, une autre bonne pratique couramment omise est de passer par un système fait pour, au lieu de nous fatiguer à stocker nous-mêmes nos valeurs dans de simples fichiers. Des solutions comme PostgreSQL ou MySQL semblent déraisonnées pour un petit programme, mais n'oublions pas les alternatives presque tout aussi légères qu'une méthode maison à coup de « fopen() » que nous offre par exemple SQLite. Beaucoup de temps sera épargné dans l'étude de la syntaxe qu'aurait eue notre format perso ainsi que dans l’implémentation des modules de gestions. De plus, une application comme SQLite ou MySQL sont très optimisées pour leur travail, et donc demeure bien souvent plus rapide et robuste que pourrait être notre création personnelle.

Rappeler le véritable but des exceptions semble aussi nécessaire. En effet, leur rôle est de bloquer le programme si une erreur technique survient plutôt que de continuer son exécution sous une forme incorrecte. Ces anomalies peuvent être obtenues si nous essayons d'atteindre l'élément 84 d'un tableau comportant uniquement 17 entrées. Ou encore, quand une base de données est hors ligne, ou qu'une bibliothèque est employée n'importe comment (paramètres invalides logiquement, par exemple). En revanche, l'exception ne doit jamais être confondue avec une simple structure conditionnelle répondant à une situation faisant partie du fonctionnement normal de l'application. Ainsi, c'est une mauvaise idée de les utiliser pour afficher un message d'erreur au client désirant effectuer un virement alors que son compte est déjà à zéro.

Gérer de gros projets.

its_complicated

Un principe de base dans de conséquents projets est d'accepter le fait qu'on n’aura jamais le temps de faire tout ce qu'on avait prévu. Chercher à réinventer la roue plutôt que d'utiliser des bibliothèques existantes ne fera qu'empirer la chose. En effet, il est important de distinguer la période d'apprentissage de celle de la concrétisation. Laissons l'expérimentation en dehors de nos applications. Elles sont déjà bien assez complexes à gérer sans avoir besoin d'ajouter des erreurs de conceptions ou des bugs bêtement.

Le second problème auquel nous devons faire face quand on évolue sur un gros projet est d'éviter de nous perdre dans sa création. Beaucoup de programmeurs codent en « mode spéculatif ». Cela signifie qu'ils ne savent pas vraiment sur quoi ils travaillent, ni combien de temps cela prendra. C'est par exemple le cas pour le gars qui dit améliorer « l'ergonomie du site sur petit écran » pendant « 2 ou 3 jours ». Il faut en effet pouvoir segmenter les objectifs de façon bien plus fine que cela. Cela nous permettra de faire assez fréquemment des commits sans compromettre la stabilité du dépôt avec des modifications défectueuses pushées parce qu'il est l'heure de rentrer. Ainsi, nous devrions savoir précisément ce que contiendra notre prochain commit, et la durée nécessaire à l'implémentation, qui ne devrait pas dépasser la journée.

Avoir des tâches trop grosses a aussi comme mauvaise habitude de favoriser l'utilisation de « fonctions interims », c'est-à-dire de fonctions écrites dans l'urgence, et qui ne respectent pas les normes de codage et de documentation mises en place dans le projet. Le but de celles-ci est de rapidement répondre à un besoin qu'on refera plus tard, de façon plus propre. C'est par exemple le genre de truc qu'on fait le soir à 17h30, pour pondre quelque chose un minimum présentable dans le commit journalier, ou parce qu'une partie autre que celle sur laquelle nous sommes sensés travailler demande des modifications pour nous permettre de réaliser notre véritable objectif. Ces choses ajoutent beaucoup de désordre ce qui rend la compréhension et la lecture difficile. On doit donc les éviter le plus possible.

the_weel

Toutes ces dettes techniques introduites peuvent vous laisser croire que tout recommencer est une bonne solution. Souvent, cette envie nous vient après une multitude d'erreurs dues à une source trop confuse, ou encore parce qu'une nouvelle technologie nous a séduites, et que refaire notre projet avec celle-ci semble très intéressant. Attention, c'est une décision à considérer, car la création déjà existante, aussi moche soit-elle, est en ce moment plutôt fonctionnelle. Elle a été testée, et représente des mois, voire des années de conception. Reprendre totalement à zéro est un choix énorme, lourd de conséquences. Ainsi, faire des réécritures partielles afin de limiter la charge de travail à faire simultanément est bien plus raisonnable. Il est également nécessaire de savoir exactement ce qui n'allait pas dans l'ancien code et quoi faire pour le rendre meilleur… En effet, nombre de restructurations se révèlent finalement être des échecs, car l'ampleur des modifications n'était pas correctement estimée, ou que d'autres subtilités ont été oubliées. Ne recommencez que si vous n'avez pas de choix alternatif possible.

Gérer les autres utilisateurs.

S'occuper de gros projets n'ajoute pas que de la complexité au code en lui-même, mais aussi dans la façon avec laquelle les différents acteurs collaborent. Effectivement, il est bien plus aisé de travailler à trois que quarante. Un autre problème de gestion des « humains » intervient quand nous développons dans l'optique d'être réemployé par d'autres.

Un des premiers pièges dans ce genre de cas concise dans la création de bibliothèques pour tout et n'importe quoi. En effet, un des mots d'ordre dans la programmation (surtout orientée objet) est « réutilisation ». Comme quoi un code n'est rien s’il ne peut être réutilisé, toussa, toussa… Il faut quand même prendre conscience que ces dépendances externes peuvent devenir très difficiles à entretenir. C'est pour cette raison qu'avoir le même code en deux exemplaires peut finalement être pertinent si ces deux algorithmes sont effectués dans des contextes totalement différents. En effet, ces similitudes sont à considérer dès lors comme des coïncidences, et rien ne permet aux développeurs de certifier que le comportement devra rester le même dans les deux cas dans le futur. Les lier par une dépendance commune est donc bien plus gênant que d'avoir à dupliquer une fonction.

Si malgré tout, le code que vous construisez nécessite une mise en bibliothèque, alors il faut penser à faire une API simple. Souvent, celles-ci sont élaborées dans le but d'être efficaces et pas trop difficiles à implémenter… C'est malheureusement oublier le rôle premier d'une API : être facilement utilisable par des personnes extérieures, qui ne connaissent rien du tout sur les techniques employé pour répondre aux caractéristiques.
Ainsi, des interfaces limpides sont à privilégier, au détriment du confort de développement pour les mainteneurs de l'API. Par exemple, une certaine aisance se retrouve dans l'implémentation d'une fonction move prenant en paramètre le sens de déplacement et la vitesse d'un robot. Mais bon, avoir des appels de la forme move(20, 1) dans le code ne constitue pas l'information la plus explicite possible pour quelqu'un ne sachant pas comment move est conçu. De plus, comment gérer un cas comme celui-ci : move(-20,1) ? Si le sens « 1 » est la marche avant, doit-on considérer une vitesse de -20 comme étant 20 en marche arrière ? Et « 20 », est-ce rapide, ou lent ? Walk_forward() et run_forward() seraient bien plus parlants pour le client que move. La clarté du nom des fonctions permettra de savoir avec précision l'action effectuée ainsi que l'étroitesse de ne pas les employer de façons invalides. Une interface doit être instinctive. Il faut qu'il soit difficile, voir impossible d'y avoir recours d'une voie imprévue. Pour cela, le mieux reste de travailler sur les interactions entre toutes les méthodes, et tous les objets pour définir ce que l'API autorisera l'usager à faire, ou non. Ensuite, celle-ci devrait être testée via une version bêta : étudier comment les gens évoluent avec notre API, et ne pas hésiter à faire les modifications nécessaires si l'on s’aperçoit que celle-ci est mal employée. Le mot d'ordre est de faire en sorte que l'usage de la bibliothèque soit naturel pour les utilisateurs, ce qui souvent est assez difficile à concevoir du point de vue du développeur.

Déploiement du projet.

creation

Dès que notre programme grandit en faisant de plus en plus de fichiers sources, ou que les ressources externes comme des images se multiplient, il faut commencer à penser à la méthode dont on doit s'y prendre pour déployer le projet de façon rapide et efficace sur des plates-formes de développement différentes.

Un moyen commode de faire ça est d'employer un gestionnaire de version qui contient absolument tout ce qui est nécessaire à la compilation. Il est aussi une bonne idée d'y inclure la documentation, les scripts divers, les tests fonctionnels, et toutes autres choses ayant un rapport. De cette façon, il suffit d'un update du dépôt pour effectuer une mise à jour de l'intégralité du projet. Il n'est ainsi plus utile d'avoir à le faire manuellement pour chaque partie, ce qui pourrait vite casser notre copie de développement en omettant quelque chose.

Pour continuer cette sensibilisation jusqu'au bout, nous pouvons aussi noter qu'une simplicité au moins similaire est requise pour permettre la compilation et la mise en production de notre programme. Ce travail est trop souvent délaissé, ce qui accroît considérablement la difficulté de celui-ci. Énormément de temps est perdu dans la formation des nouveaux arrivant à cet effet, ou en résolution de bugs dus à la modification du processus. Faire de bons scripts dont la seule exécution effectue toutes les étapes demandées pour rendre la chose opérationnelle est certes coûteux, mais restera toujours moindre par rapport à une gestion manuelle.

Pour s'assurer d'élaborer un installateur de qualité, il semble nécessaire de le concevoir en parallèle de l'évolution du projet en soit. Trop fréquemment, celui-ci est bâclé à la fin, car sa création prend plus de temps que prévu. En effet, il est souvent assez complexe de pouvoir faire tourner la même application sur le laptop de vos développeurs et sur celui de vos clients. Surtout que ces derniers exigent quelque chose de simple d'utilisation en limitant les risques d'erreurs (ils n'auront pas une impression très positive de votre logiciel si dès le premier côtoiement il y a déjà quelque chose qui cloche). Pour ces raisons, redéployer fréquemment sur une machine vierge sera un gage de bonne qualité du processus, car celui-ci aura été fait progressivement, testé, et aura inspiré l'architecture du logiciel. En effet, c'est également souvent après coup, quand on effectue justement ce genre de travaux, que l'on prend conscience à quel point il est difficile de configurer un élément, ce qui résulte en général sur une erreur d'ergonomie… Créer l'installateur en même temps permettra de remarquer le problème bien plus tôt, et nous autorisera à prendre le temps de le résoudre correctement.

Enfin, faites attention de ne pas mélanger ce qui se rapporte à l'environnement et au programme en lui même. Celui-ci ne devrait qu'être disponible en un seul binaire capable de s'adapter à n'importe quel système, en fonction du contenu d'un fichier de configuration. Trop souvent certaines équipes de développeurs fournissent des exécutables différents possédant certains services activés ou non (version debug, française, etc.). C'est très mauvais, car non seulement ça augmente le risque d'erreur par confusion, mais également à cause du fait que l'on peut effectuer certains tests ou implémentations sur une version et les oublier sur d'autres ! Concernant les fichiers de configuration, il est aussi intéressant de les stocker sur un gestionnaire de version, ça évitera ainsi de casser l'exécution sans savoir ce qu'on y a changé.


Notes :

Crédit images :