web-dev-qa-db-fra.com

Comment justifiez-vous l'écriture de plus de code en suivant des pratiques de code propres?

Note du modérateur
Cette question a déjà reçu dix-sept réponses . Avant de publier une nouvelle réponse, veuillez lire les réponses existantes et assurez-vous que votre point de vue n'est pas déjà suffisamment couvert.

J'ai suivi certaines des pratiques recommandées dans le livre "Clean Code" de Robert Martin, en particulier celles qui s'appliquent au type de logiciel avec lequel je travaille et celles qui ont du sens pour moi (je ne le considère pas comme un dogme) .

Un effet secondaire que j'ai remarqué, cependant, est que le code "propre" que j'écris, est plus de code que si je ne suivais pas certaines pratiques. Les pratiques spécifiques qui y conduisent sont:

  • Conditionnement encapsulant

Donc au lieu de

if(contact.email != null && contact.emails.contains('@')

Je pourrais écrire une petite méthode comme ça

private Boolean isEmailValid(String email){...}
  • Remplacement d'un commentaire en ligne par une autre méthode privée, de sorte que le nom de la méthode se décrit plutôt que d'avoir un commentaire en ligne par-dessus
  • Une classe ne devrait avoir qu'une seule raison de changer

Et quelques autres. Le fait est que ce qui pourrait être une méthode de 30 lignes, finit par être une classe, en raison des méthodes minuscules qui remplacent les commentaires et encapsulent les conditions, etc. Lorsque vous réalisez que vous disposez de tant de méthodes, il est "logique" mettre toutes les fonctionnalités dans une seule classe, alors que cela aurait dû être une méthode.

Je suis conscient que toute pratique poussée à l'extrême peut être nuisible.

La question concrète à laquelle je cherche une réponse est:

Est-ce un sous-produit acceptable de l'écriture de code propre? Si oui, quels sont les arguments que je peux utiliser pour justifier le fait que plus de LOC ont été écrits?

L'organisation ne se préoccupe pas spécifiquement de plus de LOC, mais plus de LOC peut entraîner de très grandes classes (qui, encore une fois, pourraient être remplacées par une longue méthode sans un tas de fonctions d'assistance à usage unique pour des raisons de lisibilité).

Quand vous voyez une classe assez grande, cela donne l'impression que la classe est suffisamment occupée et que sa responsabilité est terminée. Vous pourriez donc finir par créer plus de classes pour obtenir d'autres fonctionnalités. Le résultat est alors beaucoup de classes, toutes faisant "une chose" à l'aide de nombreuses petites méthodes d'aide.

CECI est la préoccupation spécifique ... ces classes pourraient être une seule classe qui réalise toujours "une chose", sans l'aide de nombreuses petites méthodes. Il pourrait s'agir d'une seule classe avec peut-être 3 ou 4 méthodes et quelques commentaires.

108
CommonCoreTawan

... nous sommes une très petite équipe qui prend en charge une base de code relativement grande et non documentée (dont nous avons hérité), donc certains développeurs/gestionnaires voient la valeur d'écrire moins de code pour faire avancer les choses afin d'avoir moins de code à maintenir

Ces gens ont correctement identifié quelque chose: ils veulent que le code soit plus facile à maintenir. Là où ils ont mal tourné, c'est en supposant que moins il y a de code, plus c'est facile à maintenir.

Pour que le code soit facile à maintenir, il doit être facile à modifier. De loin, le moyen le plus simple d'obtenir un code facile à changer est de disposer d'un ensemble complet de tests automatisés qui échoueront si votre changement est un changement. Les tests sont du code, donc l'écriture de ces tests va gonfler votre base de code. Et c'est une bonne chose.

Deuxièmement, pour déterminer ce qui doit être changé, votre code doit être à la fois facile à lire et à raisonner. Il est très peu probable que le code très concis, de taille réduite juste pour limiter le décompte des lignes soit facile à lire. Il y a évidemment un compromis à trouver car un code plus long prendra plus de temps à lire. Mais si c'est plus rapide à comprendre, alors ça vaut le coup. S'il n'offre pas cet avantage, alors cette verbosité cesse d'être un avantage. Mais si un code plus long améliore la lisibilité, c'est une bonne chose.

129
David Arno

Oui, c'est un sous-produit acceptable, et la justification est qu'il est maintenant structuré de telle sorte que vous n'avez pas à lire la plupart du code la plupart du temps. Au lieu de lire une fonction de 30 lignes à chaque fois que vous effectuez un changement, vous lisez une fonction de 5 lignes pour obtenir le flux global, et peut-être quelques fonctions d'assistance si votre changement touche cette zone. Si votre nouvelle classe "extra" est appelée EmailValidator et que vous savez que votre problème ne concerne pas la validation des e-mails, vous pouvez ignorer la lecture.

Il est également plus facile de réutiliser des morceaux plus petits, ce qui tend à réduire le nombre de lignes pour votre programme global. Un EmailValidator peut être utilisé partout. Certaines lignes de code qui effectuent la validation des e-mails mais sont compressées avec le code d'accès à la base de données ne peuvent pas être réutilisées.

Et ensuite, réfléchissez à ce qui doit être fait si les règles de validation des e-mails doivent être modifiées, ce que vous préférez: un emplacement connu; ou de nombreux endroits, en manquant peut-être quelques-uns?

154
Karl Bielefeldt

Bill Gates était réputé pour avoir déclaré: "Mesurer les progrès de la programmation par des lignes de code, c'est comme mesurer les progrès de la construction d'avions en poids".

Je suis humblement d'accord avec ce sentiment. Cela ne veut pas dire qu'un programme doit s'efforcer d'obtenir plus ou moins de lignes de code, mais que ce n'est finalement pas ce qui compte pour créer un programme fonctionnel et fonctionnel. Il est utile de se rappeler qu'en fin de compte, l'ajout de lignes de code supplémentaires est théoriquement plus lisible de cette façon.

Des désaccords peuvent survenir quant à savoir si un changement spécifique est plus ou moins lisible, mais je ne pense pas que vous auriez tort d'apporter un changement à votre programme parce que vous pensez qu'en faisant cela vous faites c'est plus lisible. Par exemple, faire un isEmailValid pourrait être considéré comme superflu et inutile, surtout s'il est appelé exactement une fois par la classe qui le définit. Cependant, je préfère de loin voir un isEmailValid dans une condition plutôt qu'une chaîne de conditions ANDed par laquelle je dois déterminer ce que chaque condition individuelle vérifie et pourquoi elle est vérifiée.

Lorsque vous rencontrez des problèmes, c'est lorsque vous créez une méthode isEmailValid qui a des effets secondaires ou vérifie des éléments autres que l'e-mail, car c'est pire que de simplement tout écrire. C'est pire parce que c'est trompeur et je peux manquer un bug à cause de ça.

Bien que ce ne soit clairement pas le cas dans ce cas, je vous encourage donc à continuer comme vous le faites. Vous devriez toujours vous demander si en faisant le changement, il est plus facile à lire, et si c'est votre cas, alors faites-le!

34
Neil

donc certains développeurs/gestionnaires voient la valeur d'écrire moins de code pour faire avancer les choses afin que nous ayons moins de code à maintenir

Il s'agit de perdre de vue l'objectif réel.

Ce qui compte, c'est réduction des heures consacrées au développement. Cela se mesure en temps (ou en effort équivalent), pas en lignes de code.
Cela revient à dire que les constructeurs automobiles devraient construire leurs voitures avec moins de vis, car cela prend un temps non nul pour insérer chaque vis. combien de vis il a ou n'a pas. Par-dessus tout, une voiture doit être performante, sûre et facile à entretenir.

Le reste de la réponse est des exemples de la façon dont un code propre peut entraîner des gains de temps.


Enregistrement

Prenez une application (A) qui n'a pas de journalisation. Créez maintenant l'application B, qui est la même application A mais avec une journalisation. B aura toujours plus de lignes de code, et donc vous devez écrire plus de code.

Mais beaucoup de temps passera à enquêter sur les problèmes et les bugs, et à déterminer ce qui n'a pas fonctionné.

Pour l'application A, les développeurs seront bloqués en lisant le code et devront continuellement reproduire le problème et parcourir le code pour trouver la source du problème. Cela signifie que le développeur doit tester du début de l'exécution jusqu'à la fin, dans chaque couche utilisée, et doit observer chaque élément de logique utilisé.
Peut-être qu'il a de la chance de le trouver immédiatement, mais peut-être que la réponse sera au dernier endroit où il pense à chercher.

Pour l'application B, en supposant une journalisation parfaite, un développeur observe les journaux, peut immédiatement identifier le composant défectueux et sait maintenant où chercher.

Cela peut être une question de minutes, d'heures ou de jours économisés; selon la taille et la complexité de la base de code.


régressions

Prenez l'application A, qui n'est pas du tout compatible avec DRY.
Prenez l'application B, qui est SEC, mais a fini par avoir besoin de plus de lignes en raison des abstractions supplémentaires.

Une demande de modification est déposée, ce qui nécessite une modification de la logique.

Pour l'application B, le développeur modifie la logique (unique, partagée) en fonction de la demande de changement.

Pour l'application A, le développeur doit changer toutes les instances de cette logique où il se souvient de son utilisation.

  • S'il parvient à se souvenir de toutes les instances, il devra toujours implémenter le même changement plusieurs fois.
  • S'il ne parvient pas à se souvenir de toutes les instances, vous avez maintenant affaire à une base de code incohérente qui se contredit. Si le développeur a oublié un morceau de code rarement utilisé, ce bogue peut ne pas devenir apparent pour les utilisateurs finaux avant longtemps. À ce moment-là, les utilisateurs finaux vont-ils identifier la source du problème? Même si c'est le cas, le développeur ne se souviendra peut-être pas de ce qu'impliquait le changement et devra trouver comment changer ce morceau de logique oublié. Peut-être que le développeur ne travaille même plus dans l'entreprise d'ici là, et que quelqu'un d'autre doit maintenant tout comprendre à partir de zéro.

Cela peut entraîner une perte de temps énorme. Pas seulement dans le développement, mais dans la chasse et la recherche du bug. L'application peut commencer à se comporter de manière irrégulière d'une manière que les développeurs ne peuvent pas facilement comprendre. Et cela entraînera de longues sessions de débogage.


Interchangeabilité développeur

Développeur Une application créée A. Le code n'est ni propre ni lisible, mais il fonctionne comme un charme et a été exécuté en production. Sans surprise, il n'y a pas non plus de documentation.

Le développeur A est absent pendant un mois en raison de vacances. Une demande de modification d'urgence est déposée. Il ne peut pas attendre encore trois semaines pour que Dev A revienne.

Le développeur B doit exécuter cette modification. Il doit maintenant lire l'intégralité de la base de code, comprendre comment tout fonctionne, pourquoi cela fonctionne et ce qu'il essaie d'accomplir. Cela prend des années, mais disons qu'il peut le faire en trois semaines.

En même temps, l'application B (que dev B a créée) a une urgence. Le Dev B est occupé, mais le Dev C est disponible, même s'il ne connaît pas la base de code. Qu'est-ce qu'on fait?

  • Si nous continuons B à travailler sur A et mettons C à travailler sur B, alors nous avons deux développeurs qui ne savent pas ce qu'ils font, et le travail est effectué de manière sous-optimale.
  • Si nous éloignons B de A et lui faisons faire B, et que nous plaçons maintenant C sur A, alors tout le travail du développeur B (ou une partie importante de celui-ci) peut finir par être rejeté. C'est potentiellement des jours/semaines d'efforts gaspillés.

Dev A revient de ses vacances, et voit que B n'a pas compris le code, et l'a donc mal mis en œuvre. Ce n'est pas la faute de B, car il a utilisé toutes les ressources disponibles, le code source n'était tout simplement pas suffisamment lisible. A doit-il maintenant passer du temps à corriger la lisibilité du code?


Tous ces problèmes, et bien d'autres, finissent par perte de temps. Oui, à court terme, le code propre nécessite plus d'efforts maintenant , mais il finira par payer des dividendes dans le futur lorsque des bugs/changements inévitables doivent être corrigés.

La direction doit comprendre qu'une tâche courte maintenant vous fera économiser plusieurs tâches longues à l'avenir. Ne pas planifier, c'est échouer.

Si oui, quels sont les arguments que je peux utiliser pour justifier le fait que plus de LOC ont été écrits?

Mon explication est de demander à la direction ce qu'elle préférerait: une application avec une base de code 100KLOC qui peut être développée en trois mois, ou une base de code 50KLOC qui peut être développée en six mois.

Ils choisiront évidemment le temps de développement le plus court, car la gestion ne se soucie pas de KLOC . Les gestionnaires qui se concentrent sur KLOC gèrent la microgestion tout en n'étant pas informés de ce qu'ils essaient de gérer.

23
Flater

Je pense que vous devriez très faire attention à appliquer des pratiques de "code propre" au cas où elles conduiraient à une complexité plus globale. Le refactoring prématuré est à l'origine de nombreuses mauvaises choses.

L'extraction d'un conditionnel vers une fonction conduit à un code plus simple au point d'où le conditionnel a été extrait, mais conduit à plus de complexité globale parce que vous avez maintenant une fonction qui est visible depuis plus de points dans le programme. Vous ajoutez une légère charge de complexité à toutes les autres fonctions où cette nouvelle fonction est désormais visible.

Je ne dis pas que vous ne devriez pas extraire le conditionnel, juste que vous devriez considérer attentivement si vous en avez besoin.

  • Si vous souhaitez tester spécifiquement la logique de validation des e-mails. Ensuite, vous devez extraire cette logique dans une fonction distincte - probablement même une classe.
  • Si la même logique est utilisée à plusieurs endroits dans le code, vous devez évidemment l'extraire dans une seule fonction. Ne te répète pas!
  • Si la logique est évidemment une responsabilité distincte, par ex. la validation des e-mails se produit au milieu d'un algorithme de tri. La validation des e-mails changera indépendamment de l'algorithme de tri, ils doivent donc être dans des classes distinctes.

Dans tout ce qui précède, c'est une raison pour laquelle l'extraction au-delà n'est que du "code propre". De plus, vous n'auriez probablement même pas douté que ce soit la bonne chose à faire.

Je dirais qu'en cas de doute, choisissez toujours le code le plus simple et le plus simple.

23
JacquesB

Je voudrais souligner qu'il n'y a rien de fondamentalement mauvais à cela:

if(contact.email != null && contact.email.contains('@')

En supposant au moins qu'il est utilisé cette fois.

Je pourrais avoir des problèmes avec cela très facilement:

private Boolean isEmailValid(String email){
   return email != null && email.contains('@');
}

Quelques choses que je surveillerais:

  1. Pourquoi est-ce privé? Il ressemble à un talon potentiellement utile. Est-il suffisamment utile pour être une méthode privée et aucune chance qu'elle soit utilisée plus largement?
  2. Je ne nommerais pas personnellement la méthode IsValidEmail, peut-être ContainsAtSign ou LooksVaguelyLikeEmailAddress car il ne fait pratiquement pas de véritable validation, ce qui est peut-être bon, peut-être pas ce qui est attendu.
  3. Est-il utilisé plus d'une fois?

S'il est utilisé une fois, est simple à analyser et prend moins d'une ligne, je devine la décision. Ce n'est probablement pas quelque chose que j'appellerais si ce n'était pas un problème particulier d'une équipe.

D'un autre côté, j'ai vu des méthodes faire quelque chose comme ceci:

if (contact.email != null && contact.email.contains('@')) { ... }
else if (contact.email != null && contact.email.contains('@') && contact.email.contains("@mydomain.com")) { //headquarters email }
else if (contact.email != null && contact.email.contains('@') && (contact.email.contains("@news.mydomain.com") || contact.email.contains("@design.mydomain.com") ) { //internal contract teams }

Cet exemple n'est évidemment pas SEC.

Ou même juste cette dernière déclaration peut donner un autre exemple:

if (contact.email != null && contact.email.contains('@') && (contact.email.contains("@news.mydomain.com") || contact.email.contains("@design.mydomain.com") )

L'objectif devrait être de rendre le code plus lisible:

if (LooksSortaLikeAnEmail(contact.Email)) { ... }
else if (LooksLikeFromHeadquarters(contact.Email)) { ... }
else if (LooksLikeInternalEmail(contact.Email)) { ... }

Un autre scénario:

Vous pourriez avoir une méthode comme:

public void SaveContact(Contact contact){
   if (contact.email != null && contact.email.contains('@'))
   {
       contacts.Add(contact);
       contacts.Save();
   }
}

Si cela correspond à votre logique métier et n'est pas réutilisé, il n'y a pas de problème ici.

Mais quand quelqu'un demande "Pourquoi est '@' enregistré, parce que ce n'est pas vrai!" et vous décidez d'ajouter une sorte de validation réelle, puis de l'extraire!

Vous serez heureux de l'avoir fait lorsque vous devez également prendre en compte le deuxième compte de messagerie du président Pr3 $ sid3nt @ h0m3! @ Mydomain.com et décider de tout faire et d'essayer de prendre en charge RFC 2822.

Sur la lisibilité:

// If there is an email property and it contains an @ sign then process
if (contact.email != null && contact.email.contains('@'))

Si votre code est aussi clair, vous n'avez pas besoin de commentaires ici. En fait, vous n'avez pas besoin de commentaires pour dire ce que le code fait la plupart du temps, mais plutôt pourquoi il le fait:

// The UI passes '@' by default, the DBA's made this column non-nullable but 
// marketing is currently more concerned with other fields and '@' default is OK
if (contact.email != null && contact.email.contains('@'))

Que les commentaires au-dessus d'une instruction if ou à l'intérieur d'une petite méthode soient pour moi, pédants. Je pourrais même argumenter le contraire d'utile avec de bons commentaires dans une autre méthode, car maintenant vous devrez naviguer vers une autre méthode pour voir comment et pourquoi il fait ce qu'il fait.

En résumé: ne mesurez pas ces choses; Concentrez-vous sur les principes à partir desquels le texte a été construit (SEC, SOLIDE, BAISER).

// A valid class that does nothing
public class Nothing 
{

}
9
AthomSfere

Clean Code est un excellent livre et mérite d'être lu, mais ce n'est pas l'autorité finale sur ces questions.

Décomposer le code en fonctions logiques est généralement une bonne idée, mais peu de programmeurs le font dans la mesure où Martin le fait - à un moment donné, vous obtenez des rendements décroissants en transformant tout en fonctions et il peut être difficile de suivre lorsque tout le code est en minuscule pièces.

Une option quand cela ne vaut pas la peine de créer une toute nouvelle fonction est d'utiliser simplement une variable intermédiaire:

boolean isEmailValid = (contact.email != null && contact.emails.contains('@');

if (isEmailValid) {
...

Cela permet de garder le code facile à suivre sans avoir à beaucoup sauter dans le fichier.

Un autre problème est que Clean Code devient assez vieux comme un livre maintenant. Beaucoup d'ingénierie logicielle s'est orientée vers la programmation fonctionnelle, tandis que Martin fait tout son possible pour ajouter de l'état aux choses et créer des objets. Je soupçonne qu'il aurait écrit un livre tout à fait différent s'il l'avait écrit aujourd'hui.

6
Rich Smith

Compte tenu du fait que la condition "est valide par e-mail" que vous avez actuellement accepterait l'adresse e-mail très invalide "@ ", Je pense que vous avez toutes les raisons d'abstraire une classe EmailValidator. Encore mieux, utilisez une bonne bibliothèque bien testée pour valider les adresses e-mail.

Les lignes de code en tant que métrique n'ont pas de sens. Les questions importantes en génie logiciel ne sont pas:

  • Avez-vous trop de code?
  • Avez-vous trop peu de code?

Les questions importantes sont:

  • L'application est-elle conçue correctement dans son ensemble?
  • Le code est-il correctement implémenté?
  • Le code est-il maintenable?
  • Le code est-il testable?
  • Le code est-il correctement testé?

Je n'ai jamais pensé à LoC lors de l'écriture de code à d'autres fins que Code Golf. Je me suis demandé "Pourrais-je écrire ceci plus succinctement?", Mais à des fins de lisibilité, de maintenabilité et d'efficacité, et pas simplement de longueur.

Bien sûr, je pourrais peut-être utiliser une longue chaîne d'opérations booléennes au lieu d'une méthode utilitaire, mais le devrais-je?

Votre question me fait en fait repenser à de longues chaînes de booléens que j'ai écrites et je me rends compte que j'aurais probablement dû écrire une ou plusieurs méthodes utilitaires à la place.

5
Clement Cherlin

À un niveau, ils ont raison - moins de code est meilleur. Une autre réponse citait Gate, je préfère:

"Si le débogage est le processus de suppression des bogues logiciels, alors la programmation doit être le processus de leur mise." - Edsger Dijkstra

"Lors du débogage, les novices insèrent du code correctif; les experts suppriment le code défectueux. " - Richard Pattis

Les composants les moins chers, les plus rapides et les plus fiables sont ceux qui n'existent pas. - Gordon Bell

En bref, moins vous avez de code, moins vous pouvez vous tromper. Si quelque chose n'est pas nécessaire, coupez-le.
S'il y a du code trop compliqué, simplifiez-le jusqu'à ce qu'il ne reste que les éléments fonctionnels réels.

Ce qui est important ici, c'est que tout cela se réfère à la fonctionnalité, et n'ayant que le minimum requis pour le faire. Cela ne dit rien sur comment qui est exprimé.

Ce que vous faites en essayant d'avoir du code propre n'est pas contre ce qui précède. Vous ajoutez à votre LOC mais n'ajoutez pas de fonctionnalités inutilisées.

Le but final est d'avoir du code lisible mais pas d'extras superflus. Les deux principes ne doivent pas agir l'un contre l'autre.

Une métaphore serait de construire une voiture. La partie fonctionnelle du code est le châssis, le moteur, les roues ... ce qui fait rouler la voiture. La façon dont vous brisez cela ressemble plus à la suspension, à la direction assistée et ainsi de suite, cela le rend plus facile à manipuler. Vous voulez que vos mécaniciens soient aussi simples que possible tout en effectuant leur travail, pour minimiser les risques de problèmes, mais cela ne vous empêche pas d'avoir des sièges Nice.

3
Baldrickk

Il y a beaucoup de sagesse dans les réponses existantes, mais j'aimerais ajouter un autre facteur: la langue.

Certaines langues prennent plus de code que d'autres pour obtenir le même effet. En particulier, alors que Java (que je soupçonne être le langage dans la question) est extrêmement bien connu et généralement très solide et clair et simple, certains langages plus modernes sont beaucoup plus concis et expressifs.

Par exemple, dans Java il pourrait facilement prendre 50 lignes pour écrire une nouvelle classe avec trois propriétés, chacune avec un getter et un setter, et un ou plusieurs constructeurs - alors que vous pouvez accomplir exactement la même chose dans une seule ligne de Kotlin * ou Scala. (Économie encore plus grande si vous vouliez également des méthodes equals(), hashCode() et toString() appropriées.)

Le résultat est qu'en Java, le travail supplémentaire signifie que vous êtes plus susceptible de réutiliser un objet général qui ne correspond pas vraiment, de presser des propriétés dans des objets existants, ou de passer un tas de propriétés "nues" individuellement; tandis que dans un langage concis et expressif, vous êtes plus susceptible d'écrire un meilleur code.

(Cela met en évidence la différence entre la complexité "superficielle" du code et la complexité des idées/modèles/traitements qu'il met en œuvre. Les lignes de code ne sont pas une mauvaise mesure du premier, mais ont beaucoup moins à voir avec le second .)

Le "coût" de bien faire les choses dépend donc de la langue. Peut-être qu'un signe d'une bonne langue est celui qui ne vous fait pas vous faire choisir entre bien faire les choses et les faire simplement!

(* Ce n'est pas vraiment l'endroit pour une prise, mais Kotlin vaut bien un coup d'œil à mon humble avis.)

2
gidds

La réduction de LOC s'est avérée corrélée avec des défauts réduits, rien d'autre. En supposant alors que chaque fois que vous réduisez le LOC, vous avez réduit le risque de défauts, vous tombez essentiellement dans le piège de croire que la corrélation est égale à la causalité. Le LOC réduit est un résultat de bonnes pratiques de développement et non ce qui rend le code bon.

D'après mon expérience, les personnes qui peuvent résoudre un problème avec moins de code (au niveau macro) ont tendance à être plus compétentes que celles qui écrivent plus de code pour faire la même chose. Ce que ces développeurs qualifiés font pour réduire les lignes de code, c'est utiliser/créer des abstractions et des solutions réutilisables pour résoudre les problèmes courants. Ils ne passent pas de temps à compter les lignes de code et à se demander s'ils peuvent couper une ligne ici ou là. Souvent, le code qu'ils écrivent est plus verbeux qu'il n'est nécessaire, ils en écrivent juste moins.

Laisse moi te donner un exemple. J'ai dû faire face à la logique des périodes et comment elles se chevauchent, si elles sont adjacentes et quels écarts existent entre elles. Lorsque j'ai commencé à travailler sur ces problèmes, j'avais des blocs de code faisant les calculs partout. Finalement, j'ai construit des classes pour représenter les périodes et les opérations qui ont calculé les chevauchements, les compléments, etc. Cela a immédiatement supprimé de grandes portions de code et les a transformées en quelques appels de méthode. Mais ces classes elles-mêmes n'étaient pas du tout écrites.

En clair: si vous essayez de réduire LOC en essayant de couper une ligne de code ici ou là avec plus de concision, vous vous trompez. C'est comme essayer de perdre du poids en réduisant la quantité de légumes que vous mangez. Écrivez du code facile à comprendre, à maintenir, à déboguer et à réduire le LOC par la réutilisation et l'abstraction.

1
JimmyJames

Vous avez identifié un compromis valide

Il y a donc bien un compromis ici et c'est inhérent à l'abstraction dans son ensemble. Chaque fois que quelqu'un essaie de tirer [~ # ~] n [~ # ~] lignes de code dans sa propre fonction afin de le nommer et de l'isoler, il fait simultanément lire l'appel site plus facile (en se référant à un nom plutôt qu'à tous les détails sanglants qui sous-tendent ce nom) et plus complexe (vous avez maintenant un sens qui est enchevêtré dans deux parties différentes de la base de code). "Facile" est l'opposé de "dur", mais ce n'est pas un synonyme de "simple" qui est l'opposé de "complexe". Les deux ne sont pas opposés et l'abstraction augmente toujours la complexité afin d'insérer une forme ou une autre de facilité.

Nous pouvons voir la complexité supplémentaire directement lorsqu'un changement dans les exigences métier fait fuir l'abstraction. Peut-être qu'une nouvelle logique serait allée le plus naturellement au milieu du code pré-abstrait, par exemple si le code abstrait traverse un arbre et que vous aimeriez vraiment collecter (et peut-être agir sur) une sorte d'information pendant que vous êtes traversant l'arbre. Pendant ce temps, si vous avez résumé ce code, il peut y avoir d'autres sites d'appels et l'ajout de la logique requise au milieu de la méthode peut casser ces autres sites d'appels. Vous voyez, chaque fois que nous changeons une ligne de code, nous n'avons qu'à regarder le contexte immédiat de cette ligne de code; lorsque nous changeons une méthode, nous devons Cmd-F l'ensemble de notre code source à la recherche de tout ce qui peut se casser à la suite de la modification du contrat de cette méthode, ou espérons que les tests détectent la rupture pour nous.

L'algorithme gourmand peut échouer dans ces cas

La complexité a également rendu le code dans un certain sens moins lisible plutôt que plus . Dans un travail précédent, j'ai traité une API HTTP qui était très soigneusement et précisément structurée en plusieurs couches, chaque point de terminaison est spécifié par un contrôleur qui valide la forme du message entrant, puis le remet à un gestionnaire de "couche logique métier" , qui a ensuite fait une demande auprès d'une certaine "couche de données" qui était chargée de faire plusieurs requêtes à une couche "d'objet d'accès aux données", qui était chargée de créer plusieurs délégués SQL qui répondraient réellement à votre question. La première chose que je peux dire à ce sujet était, quelque chose comme 90% du code était du passe-partout copier-coller, en d'autres termes, ce n'était pas des opérations. Ainsi, dans de nombreux cas, la lecture d'un passage de code donné était très "facile", car "oh ce gestionnaire transmet simplement la demande à cet objet d'accès aux données". Mais le fait que vous deviez sauter entre toutes ces différentes couches et lire un tas de passe-partout pour savoir si quelque chose était personnalisé pour ce cas particulier, signifiait que vous faisiez beaucoup de changement de contexte et de recherche de fichiers et d'essayer de suivre des informations que vous n'auriez jamais dû suivre, "cela s'appelle X à cette couche, il devient appelé X 'à cette autre couche, puis il s'appelle X'" dans ce autre couche. "

Je pense que lorsque je me suis arrêté, cette simple API CRUD était au stade où si vous l'imprimiez à 30 lignes par page, cela prendrait 10 à 20 manuels de cinq cents pages sur une étagère: c'était toute une encyclopédie de répétitions code. En termes de complexité essentielle, je ne suis pas sûr qu'il y ait même la moitié d'un manuel de complexité essentielle là-dedans; nous n'avions peut-être que 5-6 diagrammes de base de données pour le gérer. Faire un léger changement était une entreprise gigantesque, apprendre que c'était une entreprise gigantesque, ajouter de nouvelles fonctionnalités devenait si douloureux que nous avions en fait des fichiers de modèle standard que nous utiliserions pour ajouter de nouvelles fonctionnalités.

J'ai donc vu de première main comment rendre chaque partie très lisible et évidente peut rendre l'ensemble très illisible et non évident. Cela signifie que l'algorithme gourmand peut échouer. Vous connaissez l'algorithme gourmand, oui? "Je vais faire n'importe quelle étape au niveau local qui améliorera le plus la situation, puis je ferai confiance que je me retrouverai dans une situation globalement améliorée." C'est souvent une belle première tentative mais elle peut aussi manquer dans des contextes complexes. Par exemple, dans la fabrication, vous pouvez essayer d'augmenter l'efficacité de chaque étape particulière d'un processus de fabrication complexe - faites des lots plus importants, criez sur les personnes au sol qui semblent ne rien faire pour mettre la main sur autre chose - et cela peut souvent détruire l'efficacité globale du système.

Meilleure pratique: utilisez DRY et longueurs pour faire l'appel

(Remarque: ce titre de section est en quelque sorte une blague; je dis souvent à mes amis que lorsque quelqu'un dit "nous devrions faire X parce que les meilleures pratiques le disent ", ils représentent 90% des le temps de ne pas parler de quelque chose comme l'injection SQL ou le hachage de mot de passe ou quoi que ce soit - les meilleures pratiques unilatérales - et donc la déclaration peut être traduite dans ce 90% du temps en "nous devrions faire X parce que I dites-le . "Comme ils peuvent avoir un article de blog d'une entreprise qui a fait un meilleur travail avec X plutôt que X ', mais il n'y a généralement aucune garantie que votre entreprise ressemble à cette entreprise, et il y a généralement un autre article d'une autre entreprise qui a fait un meilleur travail avec X 'plutôt qu'avec X. Veuillez donc ne pas prendre le titre trop au sérieux.)

Ce que je recommanderais est basé sur une conférence de Jack Diederich intitulée Stop Writing Classes (youtube.com) . Il soulève plusieurs points importants dans cet exposé: par exemple, vous pouvez savoir qu'une classe n'est vraiment qu'une fonction lorsqu'elle n'a que deux méthodes publiques, et l'une d'entre elles est le constructeur/initialiseur. Mais dans un cas, il parle de la façon dont une bibliothèque hypothétique qu'il a remplacée par une chaîne pour le discours "Muffin" a déclaré sa propre classe "MuffinHash" qui était une sous-classe du type dict intégré qui Python has. L'implémentation était complètement vide - quelqu'un venait de penser, "nous pourrions avoir besoin d'ajouter des fonctionnalités personnalisées aux dictionnaires Python plus tard, introduisons une abstraction maintenant juste dans Cas."

Et sa réponse provocante était simplement: "nous pouvons toujours le faire plus tard, si nous en avons besoin".

Je pense que nous prétendons parfois que nous serons de mauvais programmeurs à l'avenir que nous ne le sommes actuellement, alors nous pourrions vouloir insérer une sorte de petite chose qui pourrait nous rendre heureux à l'avenir. Nous anticipons les besoins du futur. "Si le trafic est 100 fois plus important que ce que nous pensons qu'il sera, cette approche ne sera pas évolutive, nous devons donc investir l'investissement initial dans cette approche plus difficile qui évoluera." Très suspect.

Si nous prenons ce conseil au sérieux, nous devons déterminer quand "plus tard" est arrivé. La chose la plus évidente serait probablement d'établir une limite supérieure de la longueur des objets pour des raisons de style. Et je pense que le meilleur conseil qui reste serait d'utiliser DRY - ne vous répétez pas - avec ces heuristiques sur les longueurs de ligne pour colmater un trou dans les principes SOLID. Basé sur l'heuristique de 30 les lignes étant une "page" de texte et une analogie avec la prose,

  1. Refactorisez un chèque dans une fonction/méthode lorsque vous souhaitez le copier-coller. Comme il y a des raisons valables occasionnelles de copier-coller, mais vous devriez toujours vous sentir sale à ce sujet. Les vrais auteurs ne vous font pas relire une grosse phrase longue 50 fois tout au long du récit à moins qu'ils n'essaient vraiment de mettre en évidence un thème.
  2. Une fonction/méthode devrait idéalement être un "paragraphe". La plupart des fonctions doivent durer environ une demi-page ou 1 à 15 lignes de code, et seulement 10% de vos fonctions devraient être autorisées à s'étendre sur une page et demie, 45 lignes ou plus. Une fois que vous avez plus de 120 lignes de code et commentaires, cette chose doit être décomposée en parties.
  3. Un fichier devrait idéalement être un "chapitre". La plupart des fichiers doivent contenir 12 pages ou moins, donc 360 lignes de code et commentaires. Seulement peut-être 10% de vos fichiers devraient avoir une longueur de 50 pages ou 1 500 lignes de code et commentaires.
  4. Idéalement, la plupart de votre code devrait être en retrait avec la ligne de base de la fonction ou un niveau plus profond. Sur la base de certaines heuristiques sur l'arborescence des sources Linux, si vous êtes religieux à ce sujet, seulement 10% de votre code devrait être en retrait de 2 niveaux ou plus dans la ligne de base, moins de 5% en retrait de 3 niveaux ou plus. Cela signifie en particulier que les choses qui doivent "envelopper" une autre préoccupation, comme la gestion des erreurs dans un gros essai/capture, doivent être retirées de la logique réelle.

Comme je l'ai mentionné là-haut, j'ai testé ces statistiques par rapport à l'arborescence source actuelle de Linux pour trouver ces pourcentages approximatifs, mais ils ont également une sorte de raison dans l'analogie littéraire.

1
CR Drost

Supposons que vous travaillez actuellement avec la classe Contact. Le fait que vous écriviez une autre méthode de validation de l'adresse e-mail prouve que la classe Contact ne gère pas une seule responsabilité.

Il gère également une certaine responsabilité des e-mails, qui devrait idéalement être sa propre classe.


Une autre preuve que votre code est une fusion des classes Contact et Email est que vous ne pourrez pas tester facilement le code de validation des e-mails. Il faudra beaucoup de manœuvres pour atteindre le code de validation de l'e-mail dans une grande méthode avec les bonnes valeurs. Voir la méthode à savoir ci-dessous.

private void LargeMethod() {
    //A lot of code which modifies a lot of values. You do all sorts of tricks here.
    //Code.
    //Code..
    //Code...

    //Email validation code becoming very difficult to test as it will be difficult to ensure 
    //that you have the right data till you reach here in the method
    ValidateEmail();

    //Another whole lot of code that modifies all sorts of values.
    //Extra work to preserve the result of ValidateEmail() for your asserts later.
}

D'un autre côté, si vous aviez une classe d'e-mail distincte avec une méthode de validation par e-mail, alors pour tester votre code de validation à l'unité, vous ne feriez qu'un simple appel à Email.Validation() avec vos données de test.


Contenu bonus: discours de MFeather sur la synergie profonde entre la testabilité et une bonne conception.

1
displayName