Titre: Réduire les temps de développement sans sacrifier la qualité Auteur: LeBouquetin Date: Tue 21 Feb 2017 15:26:31 +0100 Lien: https://linuxfr.org/users/lebouquetin/journaux/reduire-les-temps-de-developpement-sans-sacrifier-la-qualite Sommaire * Avant-propos[1] * Introduction[2] * Comment réduire les temps de développement ?[3] * Réduction de temps de développement avec perte de qualité[4] * Réduction de temps de développement sans perte de qualité[5] * Quel intérêt ?[6] * Quelques techniques[7] * Technique n°1 — ne pas prendre de décision à la place du client[8] * Technique n°2 — se limiter strictement au cahier des charges[9] * Technique n°3 — réduire le périmètre fonctionnel[10] * Technique n°4 — faire du « test-driven » sur les interfaces[11] * Technique n°5 — Faire de la conception progressive sur les composants internes[12] * Technique n°6 — Diviser pour mieux régner[13] * Technique n°7 — Écrire du code simple[14] * Technique n°8 — Écrire un script de setup d'un environnement fonctionnel[15] * Conclusion[16] Avant-propos Ce journal parle de développement logiciel, de coûts, de qualité. Il est question de méthode et méthodologie plutôt que de technologies. Ce journal est un peu long… vous êtes averti(e)s ;) Introduction Je dirige une entreprise dont l'activité principale (en terme de chiffre d'affaire) est de faire de la prestation de services : développements techniques, développements d'applications distribuées et applications web sur mesure. Régulièrement les prospects et clients trouvent que les coûts sont élevés et cherchent à les réduire. C'est naturel. Le métier de la prestation de service est de vendre des jours/homme. Lorsqu'on veut réduire les coûts, le premier réflexe est donc de réduire le nombre de jours/homme. La problématique que j'essaie d'adresser ici est donc d'être capable de répondre à ces attentes, et si possible sans que ce soit au détriment de la qualité. client : dans cet article, le client est soit un client de l'entreprise soit un client interne — responsable produit, équipe marketing, utilisateur de l'outil interne, etc. anglicismes : par avance, merci de ne pas me tenir rigueur des anglicismes présents dans ce journal. Le quotidien du développeur est fait d'anglicismes — pour le meilleur et pour le pire. Comment réduire les temps de développement ? La manière la plus simple de réduire les temps de développement est d'adopter une stratégie de développement "agressive". Cette stratégie "agressive" signifie avec perte de qualité. Analogie : on pourrait faire un parallèle avec la compression d'images. Selon les algorithmes, la compression sera avec ou sans perte d'information. De la même manière, on pourra réduire les temps de développement avec ou sans perte de qualité — mais avec des niveaux de "compression" différents. Réduction de temps de développement avec perte de qualité A algoo, nous proposons 3 stratégies de développement qui essaient d'adresser différentes attentes en terme de temps de développement (mais également de qualité) : 1.le développement de "prototype". On développe un démonstrateur, qui sera jetable. Ca permet de valider un concept, faire des démos rapidement, mais sans pérennité du code. Le coût en temps est réduit au maximum puisqu'aucune réflexion sur la pérennité n'est menée. 2.le développement de mvp[17]" — produit minimum viable. C'est un concept très prisé par les startups ; pour simplifier il s'agit d'implémenter le minimum de fonctionnalités dans un produit pour le rendre commercialisable. Dans ce type de développement, les principales problématiques de pérennité seront prises en compte, mais pas les autres. On aura alors un logiciel "moyennement évolutif". 3.le développement en mode "ingénierie logicielle". C'est la stratégie de développement long-terme : on fait de l'architecture, de la conception, des tests et de l'intégration continue — tests unitaires, test d'intégration, tests fonctionnels, etc. C'est la stratégie que l'on favorise lorsqu'on intervient sur un logiciel dont la durée de vie ciblée est de plusieurs années. Les stratégies 1 et 2 sont moins couteuses que la 3, mais la qualité est partiellement sacrifiée et le niveau de finition également en faveur d'un temps de développement réduit (à court terme) Réduction de temps de développement sans perte de qualité Je vais vous présenter ici les techniques que nous mettons en oeuvre pour réduire les coûts de développement sans pour autant réduire la qualité (voire au contraire). Quel intérêt ? Réduire les coûts sans sacrifier la qualité est évidemment intéressant pour l'entreprise ou le client car : - soit vous avez la même chose pour un coût inférieur, - soit vous avez un périmètre fonctionnel plus étendu pour le même prix. Du côté des développeurs, en revanche, on a parfois l'impression qu'on essaie de brader les compétences. En réalité il ne s'agit pas de brader des compétences mais : * d'industrialiser ce qui peut l'être sans surcoût, * d'optimiser l'exploitation des compétences de chacun, des développeurs, certes, mais aussi des autres intervenants, et en particulier des clients. Quelques techniques Les différentes techniques présentées ci-dessous ne sont pas exhaustives et ne demandent qu'à être complétées via vos commentaires. Elles ne sont pas non plus systématiques car chacune s'appliquera plus ou moins bien selon le projet, l'état d'avancement, les interlocuteurs — client interne, externe, client compétent techniquement, béotien, etc. Technique n°1 — ne pas prendre de décision à la place du client Objectif : éviter à tout prix de redévelopper une fonctionnalité non conforme aux attentes Quand on développe un logiciel, on le fait toujours pour un client. Ce client peut être externe — c'est le cas classique d'un prestataire de service, interne - on développe un logiciel à partir des demandes du marketing ou du responsable produit, ou encore "perso" (on développe un outil dont on a soi-même besoin). Les cahiers des charges et spécifications ne sont jamais complets. Il y a alors systématiquement des sujets à trancher. Les développeurs ont besoin de réponses pour avancer, mais ils n'ont pas la connaissance pour décider. Je parle bien de connaissance, pas de compétence : seul le client sait ce qu'il veut, et si le développeur sait ce dont le client a réellement besoin (cas typique : un client qui ne connait rien au métier du logiciel), il n'est toutefois pas en mesure de trancher seul. Pourquoi ? Parce que la solution "idéale" qu'il aura implémentée ne sera pas celle que le client veut donc probablement que le client ne sera pas satisfait (ce qui n'est pas bon), ou alors dans le pire des cas il va vouloir que le boulot soit refait, ce qui implique "re-développement", et dans tous les cas négociations, qui peuvent être coûteuses en temps (donc couteuses tout court, et pas la partie la plus agréable). Exemple : un cahier des charges qui indiquerait "l'utilisateur doit pouvoir être notifié". * Est-ce qu'on veut des notifications par email ? des notifications sur sa webapp ? par SMS ? Toutes ? * Est-ce que l'utilisateur choisi lui-même quand et comment il est notifié ? Pour caricaturer, dans ce cas de figure, un développeur va avoir 2 réflexes : - soit il va aller droit au but - avec un système de notifications tel qu'il l'imagine, lui, - soit il va concevoir et implémenter un système complètement flexible. Dans le premier cas, la solution ne sera pas évolutive, et s'il est parti sur de mauvaises bases, c'est potentiellement tout son travail qui sera à refaire, sans compter la frustration d'avoir "travaillé pour rien" ou "de ne pas être écouté". Dans le second cas, on aura une solution "over-engineered" — de la surqualité, qui a un coût. La solution : au moindre choix, il faut demander au client. Et idéalement au client final. On a vite fait de "savoir" lorsqu'on est chef de projet ou responsable produit, mais la réalité est la même : c'est le client qui sait. Technique n°2 — se limiter strictement au cahier des charges Objectif : développer uniquement ce qui est commandé. Lorsque le client a rédigé son cahier des charges, il a pris le temps de réfléchir à ce dont il avait besoin. Idéalement, il fournira en plus du cahier des charge, un cahier des charges prévisionnel pour les versions suivantes, ou une stratégie d'évolution envisagée. Cette stratégie va permettre d'opérer des choix techniques et architecturaux mais ne doit en aucun cas générer de coûts de développement supplémentaires. Parfois, en tant que développeur, on se dit "ah mais si je fais ça de telle manière, ça prend juste quelques jours de plus et ça permet d'avoir un système plus évolutif". Et en général plus complexe, donc plus fragile. * D'une part si cette flexibilité n'est exprimée nulle part il ne faut pas la prendre en compte — il sera temps de l'implémenter le jour où elle sera vraiment demandée (si ce jour arrive), * d'autre part en règle général, plus un système est flexible, plus il est complexe, plus il est fragile. Donc plus il nécessite de tests pour être certifié "conforme". Si une certaine flexibilité est obtenue avec quelques jours de plus, il sera probablement préférable soit de ne rien faire, soit de prendre ces quelques jours pour blinder la couverture de tests automatiques. Ainsi, le code reste simple et robuste, et la future évolution sera plus facile à implémenter car toute régression sera décelée via les tests automatiques. Technique n°3 — réduire le périmètre fonctionnel Objectif : développer uniquement ce qui est nécessaire. Lorsqu'un client écrit un cahier des charges, il ne prend pas toujours le temps de prendre le recul nécessaire pour évaluer la pertinence de ses attentes. Concrètement, il va indiquer dans son cahier des charges l'ensemble des fonctionnalités qu'il attend, mais lorsqu'on présente un chiffrage mettant en relief le temps de développement associé à chaque fonctionnalité, il n'est pas rare que l'urgence de certains points devienne toute relative. Technique n°4 — faire du « test-driven » sur les interfaces Objectif : (dé)terminer les spécifications de manière exhaustive. Nous développons des applications web et des applications distribuées. Cela signifie en particulier la définition d'APIs (REST pour la majorité, mais pas nécessairement). *Note : API signifie Application Programming Interface, il s'agit des interfaces qui vont permettre de piloter votre logiciel. * Ces interfaces sont potentiellement très différentes du code qui est dessous, mais une chose est sûre : lorsqu'on entame le développement, on ne sait pas exactement ce qu'on doit faire. Dans le meilleur des cas, une partie du travail consiste à "finir les spécifications" (par exemple la gestion des cas d'erreur), dans le pire des cas, la tâche de développement consiste aussi à écrire les spécifications. La meilleure manière de faire ce travail de spécification — en tout cas de le terminer, est d'écrire les cas de test. Cela permet de se concentrer sur l'utilisation (donc le besoin) et non sur l'implémentation (la manière de faire les choses). Concrètement l'idée est d'écrire tous les cas de test puis de les exécuter à chaque nouvelle itération du développement. D'un taux initial de 100% d'erreur, on va finir par arrriver à 100% OK une fois que le développement est terminé. Ca n'aura pas coûté plus cher et le gain sera double : * une conception adaptée au besoin, * un refactoring simplifié car spécification complète (via les cas de test) et 100% testée Dans l'hypothèse où l'on aurait commencé par le développement, puis écrit les tests, outre le fait qu'on prend le taureau par les cornes (on écrit la solution avant d'avoir écrit les spécifications), le risque est d'avoir conçu une solution qui ne va pas répondre à 100% des besoins, ce qui veut dire qu'on doit faire du refactoring ou qu'on accepte une couverture partielle des besoins. Faire du "test driven" sur les interfaces est une bonne chose, mais sur le reste ? Pas forcément. Les meilleures spécifications qu'on pourra obtenir/définir seront toujours sur les interfaces. Dans ce contexte écrire les tests revient simplement à transcrire les spécifications en code. Lorsqu'on travaille sur le développement (et la conception) interne(s), on a rarement des spécifications, on va plus (+) avancer à tâtons, ce qui correspond à la technique suivante. Technique n°5 — Faire de la conception progressive sur les composants internes Objectif : avoir quelque chose de fonctionnel le plus rapidement possible. Lorsqu'on conçoit un nouveau module logiciel, on sait rarement à l'avance exactement comment il doit être conçu et comment il va se comporter. En général, on découvre la réelle complexité progressivement parce qu'on pense au fur et à mesure à l'ensemble des cas de figure. Ce qui semblait initialement simple devient (très) compliqué ; du coup la question de « refactoriser » le code devient récurrent, etc, etc. L'idée ici est donc d'avoir au plus vite quelque chose qui fonctionne, et seulement ensuite de se poser la question de comment concevoir le module pour découper la complexité. On assimile le métier via une première version "brouillon" puis on modélise ce métier proprement. Cela signifie en gros : 1.développer le composant fonctionnant dans le cas nominal en mode "proto" 2.au cours du 1. on identifie naturellement les cas d'erreur possibles / à traiter (et on les documente d'une manière ou d'une autre dans le code) 3.on a un code fonctionnel, il est alors temps : 1.de rationaliser la conception, 2.de gérer/traiter les cas d'erreur, 3.d'écrire les tests unitaires une fois les composants clairement découpés. On sépare les tâches d'assimilation métier et de modélisation logicielle. Et pour la modélisation logicielle, rien de tel que la technique suivante. Technique n°6 — Diviser pour mieux régner Objectif : simplifier la maintenance et l'évolutivité du code. Les développeurs n'aiment que rarement faire de la maintenance sur du code. Personnellement, je trouve que c'est la partie la plus passionnante du travail car les contraintes sont fortes. Bien souvent les jeunes diplômés (en particulier) ne veulent pas faire de maintenance mais veulent concevoir un logiciel entier. Et lorsqu'il s'agit de reprendre du code, plutôt que le faire évoluer ils préfèrent souvent tout refaire de zéro. Pourtant le code a une intelligence propre : il répond à des besoins métiers qui ont été accumulés en son sein, et lorsqu'on ré-écrit un logiciel complètement, c'est une grande partie de cette connaissance que l'on perd. La raison pour laquelle la maintenance n'est pas sexy est que très souvent le code est un véritable nœud de fonctionnalités et responsabilités, et lorsqu'on aperçoit un bout et que l'on essaie de tirer sur la ficelle, c'est toute la pelote qui vient. Et plus on tire, plus les nœuds se resserrent ;) Hors, ré-écrire un code complet est très coûteux, et carrément exorbitant si on considère qu'on veut réécrire à périmètre fonctionnel constant. (attention : toutes les ré-écritures n'ont pas pour objectif d'être iso-fonctionelles) La bonne solution pour faciliter la maintenance est de découper le code pour qu'il soit plus facile de le faire évoluer. le principe est simple : "un module = une responsabilité". Et inversement. Prenons un exemple concret : je veux faire un logiciel de gestion de tâches (todo). J'identifie spontanément 3 responsabilités : 1.le stockage des informations 2.l'intelligence du logiciel - le noyau 3.l'interface "utilisateur" (ça peut être une API REST) Du MVC ? Si on veut, mais c'est le niveau 1. du découpage. Imaginons, qu'on ait développé le dit logiciel en 3 modules : une interface utilisateur en QT, un module "kernel" qui implémente l'intelligence et un module "database" qui stocke dans une base SQLite. Un jour, mon client me demande s'il est possible de faire une interface web et qu'il aimerait pouvoir stocker dans une base de données MySQL, voire MongoDB. Il y a du travail, mais on va pouvoir découper ça relativement facilement. Par rapport au cas précédent, les 3 modules se décomposent désormais 7 modules (7 responsabilités) : 1.l'instanciation du "driver de données" (une fabrique) 2.le driver SQLite (qui va reprendre grosso modo le code de l'ancien composant "stockage des informations") 3.le driver MongoDB (qui va implémenter une interface compatible avec l'ancien composant "stockage des informations") 4.le noyau du logiciel (qui va désormais manipuler des objets génériques) 5.l'interface utilisateur QT 6.Un composant "web" qui implémente une API REST/Json 7.Un composant "web" frontend qui se connecte sur l'API. Les composants 1, 2 et 3 de l'ancien logiciel sont quasiment naturellement les 2, 4 et 5 du nouveau. Dans cette décomposition, chaque composant a une unique responsabilité. Et chaque responsabilité est gérée par un composant unique. Note : les parties en gras qui signifient "et inversement" mettent en relief qu'une responsabilité qui serait découpée entre plusieurs composants rend la maintenance et l'évolutivité aussi difficile que des composants aux responsabilités multiples. L'illustration caricaturale dans mon domaine est Django, qui n'incite en aucun cas à faire ce type de découpage : * Les vues Django implémentent de l'intelligence "métier" (et donc si on veut ré-implémenter les même fonctionnalités via une autre interface, c'est un gros travail de refactoring), * La validation des données va être implémentée dans des formulaires ou dans les modèles eux-même voire dans des "ModelView", * Lorsqu'on travaille avec Django Rest Framework (la seule vraie raison d'utiliser Django en 2017), les serializers risquent d'implémenter toute la logique — et de fait devenir le "kernel" partiel d'une application. Bien entendu on peut exploiter Django différemment, mais la pratique que l'on constate est celle-ci. Technique n°7 — Écrire du code simple Objectif : simplifier la maintenance et l'évolutivité du code. « Ce qui se conçoit bien s'énonce clairement » — Nicolas Boileay-Despréaux. Souvent les développeurs veulent faire des choses compliquées, et exploiter les fonctionnalités d'un langage au maximum. Mais la réalité c'est qu'un logiciel qui est écrit simplement fonctionne aussi bien qu'un logiciel exploitant toutes les particularités d'un langage. Il fonctionne aussi bien, et ça ne coûte pas plus cher à écrire. Et il coûte beaucoup moins cher à lire, donc à comprendre, donc à maintenir et faire évoluer. Parfois les arguments sont d'écrire du code concis ou du code "esthétique", mais la concision trop extrême devient inaccessible. Analogie : prenons un four qui propose 2 boutons rotatifs - un pour régler la position et un pour régler la température. Ce four est beaucoup plus simple à appréhender qu'un four avec un unique bouton rotatif sur lequel on peut "cliquer" pour entrer dans des menus. Esthétiquement certains diront que c'est plus joli - c'est subjectif. Par contre ce qui n'est pas subjectif, c'est que c'est moins naturel à utiliser et moins ergonomique. Pour le confirmer, il suffit de regarder ce qui se fait dans les cuisines des restaurants : on industrialise la cuisine, et les choix qui sont opérés sont représentatif : du gaz, des boutons ronds, un bouton par fonctionnalité. Quand vous codez, imaginez que vous êtes un chef étoilé :) Technique n°8 — Écrire un script de setup d'un environnement fonctionnel Objectif : monter un environnement de test en 1 minute Dans l'esprit du Joel Test[18], il est de bon ton d'écrire un script de setup qui permet d'obtenir un environnement fonctionnel en une commande voire deux. Cela permet de très rapidement intégrer un nouveau développeur (ou un nouveau poste de développement), cela permet aussi très rapidement de mettre au point un nouvel environnement de test, totalement vierge. Cela réduit le frein au test, et augmente donc le nombre de tests et donc le nombre de bugs détectés (et probablement aussi le nombre de bugs résolus). Plus on peut tester, plus on trouve de bugs, moins la qualité diminue. Écrire un tel script prendra probablement quelques heures en début de projet, mais ces heures "perdues" seront largement compensées par les heures perdues à mettre en place des environnements, à tester ou ne pas tester car le setup est trop pénible. Inutile d'en dire plus. Conclusion Selon les cas et les projets, nous n'appliquons pas nécessairement toutes ces techniques. Un point à prendre également en compte et qui n'apparait pas ici : derrière l'idée de réduire les temps de développement se cachent différents aspects. On y trouvera naturellement des notions de coûts — moins de jours de développement coûtent moins cher (c'est le cas initial à l'origine de ce journal), mais on y trouve aussi des notions de time-to-market. Moins de temps de développement, c'est une arrivée sur le marché plus rapide. Ce qui génère potentiellement des gains différents. Le time-to-market… c'est notamment la raison pour laquelle pas mal de startup fabriquent une première version en mode "mvp" voire carrément en mode "proto" : cela permet de prendre les parts de marché initiales, quitte à complètement redévelopper le produit par la suite — une fois que le marché a été pris, la concurrence devient difficile, la startup est devenue rentable et les dépenses de R&D nécessaires à la construction d'un système propre et robuste deviennent envisageables. Télécharger ce contenu au format Epub[19] Lire les commentaires[20] Liens: [1]: http://linuxfr.org/journaux.atom#avant-propos (lien) [2]: http://linuxfr.org/journaux.atom#introduction (lien) [3]: http://linuxfr.org/journaux.atom#comment-r%C3%A9duire-les-temps-de-d%C3%A9veloppement (lien) [4]: http://linuxfr.org/journaux.atom#r%C3%A9duction-de-temps-de-d%C3%A9veloppement-avec-perte-de-qualit%C3%A9 (lien) [5]: http://linuxfr.org/journaux.atom#r%C3%A9duction-de-temps-de-d%C3%A9veloppement-sans-perte-de-qualit%C3%A9 (lien) [6]: http://linuxfr.org/journaux.atom#quel-int%C3%A9r%C3%AAt (lien) [7]: http://linuxfr.org/journaux.atom#quelques-techniques (lien) [8]: http://linuxfr.org/journaux.atom#technique-n1--ne-pas-prendre-de-d%C3%A9cision-%C3%A0-la-place-du-client (lien) [9]: http://linuxfr.org/journaux.atom#technique-n2--se-limiter-strictement-au-cahier-des-charges (lien) [10]: http://linuxfr.org/journaux.atom#technique-n3--r%C3%A9duire-le-p%C3%A9rim%C3%A8tre-fonctionnel (lien) [11]: http://linuxfr.org/journaux.atom#technique-n4--faire-du-test-driven-sur-les-interfaces (lien) [12]: http://linuxfr.org/journaux.atom#technique-n5--faire-de-la-conception-progressive-sur-les-composants-internes (lien) [13]: http://linuxfr.org/journaux.atom#technique-n6--diviser-pour-mieux-r%C3%A9gner (lien) [14]: http://linuxfr.org/journaux.atom#technique-n7--%C3%89crire-du-code-simple (lien) [15]: http://linuxfr.org/journaux.atom#technique-n8--%C3%89crire-un-script-de-setup-dun-environnement-fonctionnel (lien) [16]: http://linuxfr.org/journaux.atom#conclusion (lien) [17]: https://fr.wikipedia.org/wiki/Produit_minimum_viable (lien) [18]: https://www.joelonsoftware.com/2000/08/09/the-joel-test-12-steps-to-better-code/ (lien) [19]: https://linuxfr.org/users/lebouquetin/journaux/reduire-les-temps-de-developpement-sans-sacrifier-la-qualite.epub (lien) [20]: https://linuxfr.org/users/lebouquetin/journaux/reduire-les-temps-de-developpement-sans-sacrifier-la-qualite#comments (lien)