Pour quelqu'un qui n'a pas encore beaucoup d'expérience dans le monde réel, la notion de code maintenable est un peu vague, même si elle découle de règles de bonne pratique typiques. Intuitivement, je peux dire que le code peut être bien écrit, mais pas maintenable si, par exemple, il relie des informations qui sont soumises à des changements constants, mais j'ai encore du mal à regarder le code et à décider s'il est maintenable ou non.
La question est donc: qu'est-ce qui nuit à la maintenabilité? Que dois-je rechercher?
Il existe de nombreux aspects de la maintenabilité, mais l'OMI les plus importants sont couplage lâche et cohésion élevée . Essentiellement, lorsque vous travaillez avec un bon code, vous pouvez apporter de petites modifications ici et là sans avoir à garder la base de code entière dans votre tête. Avec un mauvais code, il faudrait prendre plus de choses en compte: réparer ici, et ça casse ailleurs.
Lorsque vous avez plus de 100 kLOC de code devant vous, cela est crucial. Les règles de "bonnes pratiques" souvent mentionnées comme le formatage du code, les commentaires, la dénomination des variables, etc. sont superficielles par rapport aux problèmes de couplage/cohésion.
Le problème avec le couplage/cohésion est qu'il n'est pas facile à mesurer ou à voir rapidement. Il y a quelques tentatives académiques pour le faire, et peut-être quelques outils d'analyse de code statique, mais rien de ce que je sais ne donnerait immédiatement une estimation fiable de la difficulté avec laquelle vous allez avoir du code.
Code dupliqué:
Copy Paste est probablement l'opération la plus coûteuse en ce qui concerne les coûts de maintenance des programmes.
Pourquoi s'embêter à déplacer ce code vers un composant commun, je vais juste le recopier et en finir Oui ... le programmeur a probablement économisé une heure de travail ou de faire les choses de cette façon. Cependant, plus tard vient un bug et ... oui, bien sûr, il est corrigé dans seulement la moitié du code en cours d'exécution. Ce qui signifie que plus tard encore une régression sera enregistrée, ce qui entraînera un autre correctif DE LA MÊME CHOSE.
Celui-ci semble évident, mais dans le grand schéma des choses, il est insidieux. Les programmeurs ont l'air bien car ils en font plus. Le responsable de l'équipe de développement a l'air bien car il a livré à temps. Et puisque le problème ne se trouve que bien plus tard, la faute ne revient jamais au vrai coupable.
J'ai perdu le compte du nombre de fois où j'ai dû arrêter une version à cause de telles "régressions" Je viens de perdre une journée entière à suivre un correctif qui était déjà terminé, faites-le réparer à nouveau et poussez la nouvelle construction à travers les cerceaux. Donc oui, une heure pour un dev il y a six mois (environ CAD 40 $) coûtait juste une journée complète pour 1 dev senior + demi-journée pour un dev junior + une journée complète de retard dans un projet déjà en retard ....
vous faites le calcul....
donc le suivant qui me tire ça je jure qu'il vaut mieux courir vite car je serai juste derrière lui !!!
Alors que le code qui enfreint les règles dans les autres réponses est certainement pire à maintenir, tout le code est difficile à maintenir , donc moins vous en avez, mieux c'est . Les défauts sont fortement corrélés avec la quantité de code, donc plus vous avez de code, plus vous avez de bugs . Une fois que le code a dépassé une certaine taille, vous ne pouvez plus garder toute votre conception dans votre tête et les choses deviennent beaucoup plus difficiles. Enfin, plus de code signifie finalement plus d'ingénieurs, ce qui signifie plus de coûts, plus de problèmes de communication et plus de problèmes de gestion.
Mais gardez à l'esprit qu'il ne faut pas écrire de code plus complexe mais plus court. Le code le plus facile à maintenir est simple et ne contient simplement aucune redondance pour le garder aussi court que possible. La délégation de tâches à d'autres programmes ou API peut également être une solution viable tant que les appels d'API sont assez simples.
donc en un mot, tout code superflu est un problème de maintenabilité
Ironiquement, les tentatives explicites de "pérenniser" un système le rendent souvent plus difficile à maintenir.
Ne pas coder en dur les nombres magiques ou les chaînes est une chose, mais allez plus loin et vous commencez à mettre de la logique dans les fichiers de configuration. Ce qui signifie que vous ajoutez un analyseur et un interprète pour un nouveau langage de programmation obscur (et probablement mal conçu) à votre charge de maintenance.
Des couches d'abstraction et d'indirection ajoutées sans autre raison pour lesquelles certains composants pourraient être modifiés à l'avenir et vous ne voulez pas en dépendre directement sont un pur poison de maintenance et un excellent exemple pour - YAGNI . Le SLF4J FAQ explique les problèmes avec cette idée trop courante:
Étant donné que l'écriture d'un wrapper de journalisation ne semble pas si difficile, certains développeurs seront tentés d'enrouler SLF4J et de le lier uniquement s'il est déjà présent sur le chemin de classe, faisant de SLF4J une dépendance facultative de Wombat. En plus de résoudre le problème de dépendance, l'encapsuleur isolera Wombat de l'API de SLF4J, garantissant que la journalisation dans Wombat est pérenne.
D'un autre côté, tout wrapper SLF4J par définition dépend de SLF4J. Il est lié à avoir la même API générale. Si à l'avenir une nouvelle API de journalisation considérablement différente apparaît, le code qui utilise le wrapper sera tout aussi difficile à migrer vers la nouvelle API que le code qui utilisait directement SLF4J. Ainsi, l'encapsuleur n'est pas susceptible de pérenniser votre code, mais de le rendre plus complexe en ajoutant une indirection supplémentaire au-dessus de SLF4J, qui est une indirection en soi.
La dernière demi-phrase fait allusion à l'ironie douce que SLF4J lui-même est un wrapper de journalisation auquel cet argument s'applique tout aussi bien (sauf qu'il fait une tentative crédible pour être le seul wrapper dont vous avez besoin qui puisse s'asseoir au-dessus de et dessous toutes les autres Java).
D'après mon expérience, un facteur majeur de maintenabilité est cohérence. Même si le programme fonctionne de manière étrange et absurde, il est toujours plus facile à maintenir s'il le fait de la même manière partout. Travailler avec du code qui utilise différents schémas de dénomination, modèles de conception, algorithmes, etc. pour des tâches similaires, selon qui l'a écrit quand, est un PITA majeur.
Je crois que le code qui n'est pas testé unitaire nuit à la maintenabilité. Lorsque le code est couvert par des tests unitaires, vous pouvez être sûr que lorsque vous le modifiez, vous savez ce que vous cassez, car les tests unitaires associés échoueront.
Avoir des tests unitaires avec les autres suggestions ici rend votre code robuste et vous assurez que vous savez que lorsque vous changez, quels sont exactement les impacts que vous rencontrez.
La seule vraie façon d'apprendre ce qui est du code maintenable est de s'impliquer dans la maintenance d'une grande base de code (à la fois les corrections de bugs et les demandes de fonctionnalités).
Vous découvrirez:
J'ai quelques "cauchemars" qui, bien que je ne les ai jamais mis en tête de liste, ont certainement causé des difficultés.
Le méga-objet/page/fonction : (Le soi-disant " God Object " anti-pattern =): Mettre tout dans la méthode d'un objet (principalement Java world), page (principalement php et asp), méthode (principalement VB6).
Je devais conserver le code d'un programmeur qui avait dans un script php: code html, logique métier, appels à mysql tout foiré. Quelque chose comme:
foeach( item in sql_query("Select * from Items")) {
<a href="/go/to/item['id']">item['name']</a>
}
S'appuyer sur des caractéristiques obscures du langage qui pourraient ne pas être bien connues ou sur le point de devenir obsolètes.
Cela s'était reproduit avec php et des variables prédéfinies. Petite histoire: pour certains paramètres de php v4, un paramètre de requête a été automatiquement attribué à une variable. donc http://mysite.com?id=44 génèrerait "par magie" $ id variable avec la valeur '42'.
C'était un cauchemar de maintenance non seulement parce que vous ne pouviez pas savoir d'où venaient les données, mais aussi parce que php v5 les avait complètement supprimées (+1), de sorte que les personnes formées après cette version, ne comprendraient même pas ce qui se passait et pourquoi.
Ce qui rend la maintenance nécessaire, c'est que les exigences sont une cible mobile . J'aime voir le code là où de futurs changements possibles ont été anticipés et réfléchir à la façon de les gérer s'ils se produisent.
J'ai une mesure de maintenabilité. Supposons qu'une nouvelle exigence se présente ou une modification d'une exigence existante. Ensuite, les modifications apportées au code source sont implémentées, ainsi que la correction des bogues associés, jusqu'à ce que l'implémentation soit complète et correcte. Exécutez maintenant un diff entre la base de code après et la base de code avant. Si cela inclut des modifications de documentation, incluez-les dans la base de code. Dans le diff, vous pouvez obtenir un nombre N du nombre d'insertions, de suppressions et de remplacements de code nécessaires pour effectuer le changement.
Plus N est petit, en moyenne approximative par rapport aux changements passés et futurs des exigences, plus le code est maintenable. La raison en est que les programmeurs sont des canaux bruyants. Ils font des erreurs. Plus ils doivent faire de changements, plus ils font d'erreurs, qui deviennent tous des bogues, et chaque bogue est plus difficile à trouver et à corriger qu'il ne l'est en premier lieu.
Je suis donc d'accord avec les réponses qui disent suivez Ne vous répétez pas (DRY) ou ce qu'on appelle le code coupe-cookie.
Je suis également d'accord avec le mouvement vers les langages spécifiques au domaine (DSL) fourni ils réduisent N. Parfois, les gens supposent que le but d'un DSL est d'être "convivial pour le codeur" en traitant avec "abstractions de haut niveau". Cela ne réduit pas nécessairement N. La façon de réduire N est d'entrer dans un langage (qui peut être juste des choses définies au-dessus d'un langage existant) qui correspond plus étroitement aux concepts du domaine problématique.
La maintenabilité ne signifie pas nécessairement que n'importe quel programmeur peut plonger directement. L'exemple que j'utilise de ma propre expérience est Exécution différentielle . Le prix est une courbe d'apprentissage importante. La récompense est une réduction de l'ordre de grandeur du code source pour les boîtes de dialogue d'interface utilisateur, en particulier celles qui ont des éléments à changement dynamique. Les changements simples ont N autour de 2 ou 3. Les changements plus complexes ont N autour de 5 ou 6. Quand N est petit comme ça, la probabilité d'introduire des bogues est très réduite, et cela donne l'impression que cela "fonctionne juste".
À l'autre extrême, j'ai vu du code où N était généralement compris entre 20 et 30.
Ce n'est pas vraiment une réponse mais un long commentaire (car ces réponses ont déjà été présentées).
J'ai trouvé que lutter religieusement pour deux facteurs - des interfaces propres et utilisables et aucune répétition ont amélioré mon code et, avec le temps, ont fait de moi un bien meilleur programmeur.
Parfois, l'élimination du code redondant est difficile, cela vous oblige à trouver des modèles délicats.
J'analyse habituellement ce qui DOIT changer, puis j'en fais mon objectif. Par exemple, si vous faites une interface graphique sur un client pour mettre à jour une base de données - de quoi avez-vous besoin pour en ajouter un "autre" (un autre contrôle lié à la base de données?) Vous devez ajouter une ligne à la base de données et vous avez besoin la position du composant, c'est tout.
Donc, si c'est votre strict minimum, je ne vois AUCUN code - vous DEVRIEZ pouvoir le faire sans toucher à votre base de code. C'est incroyablement difficile à atteindre, quelques boîtes à outils le feront (généralement mal), mais c'est un objectif. Est-il difficile de se rapprocher? Pas terriblement. Je peux le faire et l'ai fait avec zéro code de deux manières, l'une en marquant les nouveaux composants de l'interface graphique avec le nom de la table dans la base de données, l'autre en créant un fichier XML - le fichier XML (ou YAML si vous détestez XML) peut être très utile car vous pouvez lier des validateurs et des actions spéciales au champ, ce qui rend le modèle extrêmement flexible.
De plus, la mise en œuvre correcte des solutions ne prend pas plus de temps - au moment où vous avez expédié la plupart des projets, c'est en fait moins cher.
Je peux souligner que si vous comptez beaucoup sur les Setters & Getters ("Bean Patterns"), les classes internes Generics & Anonymous, vous n'allez probablement PAS coder génériquement comme ceci. Dans les exemples ci-dessus, essayer de forcer l'un de ces éléments vous fera vraiment foirer. Les Setters & Getters vous obligent à utiliser du code pour de nouveaux attributs, les génériques vous obligent à instancier des classes (qui nécessitent du code) et les classes internes anonymes ne sont généralement pas faciles à réutiliser ailleurs. Si vous codez vraiment de manière générique, ces constructions de langage ne sont pas mauvaises, mais elles peuvent rendre difficile la visualisation d'un modèle. Pour un exemple totalement insensé:
user.setFirstName(screen.getFirstName());
user.setLastName(screen.getLastName());
Ça a l'air bien - pas vraiment redondant du tout - du moins pas d'une manière que vous pouvez corriger, non? Mais cela vous oblige à ajouter une ligne lorsque vous voulez ajouter un "deuxième prénom", c'est donc du "code redondant"
user.getNameAttributesFrom(screen);
N'a pas besoin de nouveau code pour cette tâche - cela nécessite simplement que certains attributs dans "screen" soient étiquetés avec un attribut "Name", mais maintenant nous ne pouvons pas copier l'adresse, que diriez-vous de ceci:
user.getAttributesFrom(screen, NAME_FIELDS, ADDRESS_FIELDS);
Plus agréable, un var-args vous permet d'inclure un groupe d'attributs (à partir d'une énumération) à rassembler à partir de l'écran - vous devez toujours modifier le code pour modifier les types d'attributs que vous souhaitez. Notez que "NAME_FIELDS" est une instance d'énumération simple - les champs de "screen" sont étiquetés avec cette énumération lorsqu'ils sont conçus pour les placer dans la ou les catégories correctes - il n'y a pas de table de conversion.
Attribute[] attrs=new Attributes[]{NAME_FIELDS, ADDRESS_FIELDS, FRIENDS_FIELDS};
user.getAttributesFrom(screen, attrs);
Vous en êtes maintenant à l'endroit où vous modifiez simplement les "données". C'est là que je le laisse habituellement - avec les données dans le code - parce que c'est "assez bien". L'étape suivante consiste à externaliser les données si cela est nécessaire, afin que vous puissiez les lire à partir d'un fichier texte, mais c'est rarement le cas. Les refactorings "Roll up" comme ça beaucoup une fois que vous avez pris l'habitude, et ce que je viens de faire là-bas a créé une nouvelle énumération et un nouveau modèle qui finiront par rouler dans de nombreux autres refactorings.
Enfin, notez que les bonnes solutions génériques à des problèmes comme celui-ci ne dépendent PAS de la langue. J'ai implémenté une solution sans code par emplacement pour analyser une interface graphique de texte à partir de la ligne de commande d'un routeur, la mettre à jour à l'écran et la réécrire sur l'appareil - en VB = 3. Il suffit de se consacrer au principe de ne jamais écrire de code redondant - même si vous devez écrire deux fois plus de code pour le faire!
L'interface propre (entièrement factorisée et ne permet pas le passage de choses illégales) est également importante. Lorsque vous factorisez correctement une interface entre deux unités de code, cela vous permet de manipuler le code de chaque côté de l'interface en toute impunité et permet aux nouveaux utilisateurs de mettre en œuvre les interfaces proprement.
en plus des modèles mentionnés, l'une des choses les plus importantes est
code lisible, chaque ligne de code que vous mettez dans une base de code sera lue plus de 100 fois et lorsque les gens recherchent des bogues, vous ne voulez pas qu'ils passent 30 minutes pour essayer de comprendre ce qu'il fait chacun temps.
Donc, n'essayez pas d'être intelligent avec du code "optimisé" à moins qu'il ne s'agisse vraiment d'un goulot d'étranglement, puis de le commenter et de mettre le code lisible (et fonctionnel) d'origine dans les commentaires (ou le chemin de compilation alternatif pour les tests) comme référence pour ce qu'il devrait faire.
cela comprend la fonction descriptive, les noms de paramètres et de variables et les commentaires expliquant POURQUOI un certain algorithme est choisi par rapport à un autre
Je pense que la maintenabilité est également étroitement liée à la complexité du problème, qui peut être une question subjective. J'ai vu des situations où le développeur unique a réussi à maintenir et à développer régulièrement une grande base de code, mais lorsque d'autres interviennent à sa place, cela semble un gâchis impossible à maintenir - simplement parce qu'ils ont des modèles mentaux très différents.
Les modèles et les pratiques aident vraiment (voir les autres réponses pour de bons conseils). Cependant, leur abus peut entraîner encore plus de problèmes lorsque la solution d'origine est perdue derrière les façades, les adaptateurs et les abstractions inutiles.
En général, la compréhension vient avec l'expérience. Un excellent moyen d'apprendre est d'analyser comment d'autres ont résolu des problèmes similaires, en essayant de trouver des points forts et des points faibles dans leur mise en œuvre.
Presque tout ce qui viole certains bons principes de développement logiciel nuit à la maintenabilité. Si vous regardez du code et que vous souhaitez évaluer son maintenabilité, vous pouvez choisir des principes et vérifier à quel point il les viole.
Le principe SEC est un bon point de départ: combien d'informations sont répétées dans le code? YAGNI est également très utile: combien de fonctionnalités y a-t-il qui ne sont pas nécessaires?
Si vous effectuez des recherches sur SEC et YAGNI vous trouverez d'autres principes, qui sont applicables en tant que principes généraux de développement de logiciels. Si vous utilisez un langage de programmation orienté objet et que vous souhaitez être plus spécifique, les principes SOLIDE donnent de bonnes directives pour une bonne conception orientée objet.
Le code qui viole l'un de ces principes tend à être moins maintenable. Je mets l'accent sur tendance ici, car - en général - il y a des situations où il est légitime de (légèrement) violer tout type de principe, surtout s'il est fait dans un souci de maintenabilité.
Ce qui aide aussi, c'est de chercher le code sent . Jetez un oeil dans Refactoring - Améliorer la conception du code existant de Martin Fowler. Vous y trouverez des exemples d'odeurs de code, qui affinent votre sens pour un code moins facile à gérer.
Trop de fonctionnalités.
Les fonctionnalités elles-mêmes nuisent par nature à la maintenabilité, car vous devez écrire du code supplémentaire pour les implémenter, mais une application sans fonctionnalités est inutile, nous ajoutons donc des fonctionnalités. Cependant, en omettant les fonctionnalités inutiles, vous pouvez maintenir votre code plus maintenable.