Donc, en regardant autour de moi plus tôt, j'ai remarqué quelques commentaires sur les mauvaises méthodes.
Je ne suis pas sûr d'être toujours d'accord sur le fait que les méthodes longues sont mauvaises (et j'aimerais avoir l'avis des autres).
Par exemple, j'ai quelques Django vues qui font un peu de traitement des objets avant de les envoyer à la vue, une longue méthode étant de 350 lignes de code. J'ai mon code écrit pour qu'il traite avec les paramètres - trier/filtrer le jeu de requêtes, puis petit à petit faire un peu de traitement sur les objets que ma requête a retournés.
Donc, le traitement est principalement une agrégation conditionnelle, qui a des règles suffisamment complexes pour que cela ne puisse pas être fait facilement dans la base de données, j'ai donc certaines variables déclarées en dehors de la boucle principale, puis modifiées pendant la boucle.
variable_1 = 0
variable_2 = 0
for object in queryset :
if object.condition_condition_a and variable_2 > 0 :
variable 1+= 1
.....
...
.
more conditions to alter the variables
return queryset, and context
Donc, selon la théorie, je devrais factoriser tout le code en méthodes plus petites, de sorte que la méthode de vue ait une longueur maximale d'une page.
Cependant, après avoir travaillé sur différentes bases de code dans le passé, je trouve parfois que cela rend le code moins lisible, lorsque vous devez constamment passer d'une méthode à l'autre pour en comprendre toutes les parties, tout en gardant la méthode la plus externe dans votre tête.
Je trouve qu'en ayant une longue méthode bien formatée, vous pouvez voir la logique plus facilement, car elle ne se cache pas dans les méthodes internes.
Je pourrais factoriser le code en méthodes plus petites, mais souvent il y a une boucle interne utilisée pour deux ou trois choses, donc cela entraînerait un code plus complexe, ou des méthodes qui ne font pas une chose mais deux ou trois (alternativement Je pourrais répéter les boucles internes pour chaque tâche, mais il y aura ensuite un impact sur les performances).
Existe-t-il donc un cas où les méthodes longues ne sont pas toujours mauvaises? Y a-t-il toujours lieu de rédiger des méthodes, alors qu'elles ne seront utilisées qu'en un seul endroit?
MISE À JOUR: On dirait que j'ai posé cette question il y a plus d'un an.
J'ai donc refactorisé le code après la réponse (mixte) ici, je l'ai divisé en méthodes. Il s'agit d'une application Django récupérant des ensembles complexes d'objets liés de la base de données, donc l'argument de test est sorti (il aurait probablement fallu presque toute l'année pour créer des objets pertinents pour les cas de test. I avoir un environnement de travail de type "cela doit être fait hier" avant que quiconque se plaigne.
avant :
#comment 1
bit of (uncomplicated) code 1a
bit of code 2a
#comment 2
bit of code 2a
bit of code 2b
bit of code 2c
#comment 3
bit of code 3
maintenant:
method_call_1
method_call_2
method_call_3
def method_1
bit of (uncomplicated) code 1a
bit of code 2a
def method_2
bit of code 2a
bit of code 2b
bit of code 2c
def method_3
bit of code 3
Non, les longues méthodes ne sont pas toujours mauvaises.
Dans le livre Code Complete, il est mesuré que les longues méthodes sont parfois plus rapides et plus faciles à écrire, et ne conduisent pas à des problèmes de maintenance.
En fait, ce qui est vraiment important, c'est de rester DRY et de respecter la séparation des préoccupations. Parfois, le calcul est juste long à écrire, mais ne causera vraiment aucun problème à l'avenir.
Cependant, d'après mon expérience personnelle, la plupart des méthodes longues ont tendance à ne pas séparer les préoccupations. En fait, les longues méthodes sont un moyen facile de détecter que quelque chose PEUT être erroné dans le code, et qu'une attention particulière est requise ici lors de la révision du code.
EDIT: Au fur et à mesure des commentaires, j'ajoute un point intéressant à la réponse. Je vérifierais en fait aussi les métriques de complexité de la fonction (NPATH, complexité cyclomatique ou encore mieux CRAP).
En fait, je recommande de ne pas vérifier ces métriques sur les fonctions longues, mais d'inclure des alertes sur celles-ci avec des outils automatisés (tels que checkstyle pour Java par exemple) SUR CHAQUE FONCTION.
La majeure partie de l'attention semble ici être autour de la Parole toujours. Oui, les absolus sont mauvais, et le génie logiciel est presque autant de l'art que de la science, et tout ça ... mais je vais devoir dire que pour l'exemple que vous avez donné, la méthode serait meilleure si elle était divisée vers le haut. Ce sont les arguments que j'utilise généralement pour justifier le fractionnement de votre méthode:
Lisibilité: Je ne suis pas sûr des autres, mais je ne peux pas lire 350 lignes de code rapidement. Oui, si c'est mon propre code, et que je peux faire beaucoup d'hypothèses à ce sujet, je pourrait le parcourir très rapidement, mais c'est d'ailleurs le point. Considérez combien cette méthode serait plus facile à lire si elle consistait en 10 appels à d'autres méthodes (chacune avec un nom descriptif). Pour ce faire, vous avez introduit une superposition dans le code et la méthode de haut niveau donne au lecteur un bref aperçu doux de ce qui se passe.
Edit - pour mettre cela sous un jour différent, pensez-y comme ceci: comment expliqueriez-vous cette méthode à un nouveau membre de l'équipe? Il a sûrement une structure un peu que vous pouvez résumer dans le sens de "eh bien, ça commence par faire A, puis B, puis parfois C, etc.". Le fait d'avoir une courte méthode de "vue d'ensemble" appelant d'autres méthodes rend cette structure évidente. Il est extrêmement rare de trouver 350 lignes de code qui n'en bénéficient pas; le cerveau humain n'est pas censé traiter des listes de centaines d'articles, nous les regroupons.
Réutilisation: Les méthodes longues ont tendance à avoir une faible cohésion - elles souvent font plus d'une chose. Une faible cohésion est l'ennemi de la réutilisation; si vous combinez plusieurs tâches en une seule méthode, elle finira par être réutilisée dans moins d'endroits qu'elle n'aurait dû l'être.
Testabilité et cohésion: J'ai mentionné complexité cyclomatique dans un commentaire ci-dessus - c'est une assez bonne mesure de la complexité de votre méthode. Il représente la limite inférieure du nombre de chemins uniques à travers votre code, selon les entrées (modifier: corrigé selon le commentaire de MichaelT). Cela signifie également que pour tester correctement votre méthode, vous devez avoir au moins autant de cas de test que votre numéro de complexité cyclomatique. Malheureusement, lorsque vous assemblez des morceaux de code qui ne dépendent pas vraiment les uns des autres, il n'y a aucun moyen d'être sûr de ce manque de dépendance, et la complexité a tendance à se multiplier. Vous pouvez considérer cette mesure comme une indication du nombre de choses différentes que vous essayez de faire. S'il est trop haut, il est temps de diviser et de conquérir.
Refactorisation et structure: Les méthodes longues sont souvent des signes de manque de structure dans le code. Souvent, le développeur ne pouvait pas comprendre les points communs entre les différentes parties de cette méthode, et où une ligne pouvait être tracée entre eux. Se rendre compte qu'une méthode longue est un problème, et essayer de la diviser en méthodes plus petites est la première étape sur une route plus longue pour réellement identifier une meilleure structure pour le tout. Vous devez peut-être créer une classe ou deux; ça ne va pas forcément être plus complexe au final!
Je pense également que dans ce cas, l'excuse pour avoir une méthode longue est "... certaines variables déclarées en dehors de la boucle principale sont ensuite modifiées pendant la boucle". Je ne suis pas un expert en Python, mais je suis assez certain que ce problème pourrait être trivialement résolu par une certaine forme de passage par référence.
Les méthodes longues sont toujours mauvaises, mais sont parfois meilleures que les alternatives.
Les méthodes longues sont une odeur de code . Ils indiquent généralement que quelque chose ne va pas, mais ce n'est pas une règle stricte et rapide. Habituellement, les cas où ils sont justifiés impliquent beaucoup de règles commerciales étatiques et assez complexes (comme vous l'avez constaté).
Quant à votre autre question, il est souvent utile de séparer des morceaux de logique en méthodes distinctes, même si elles ne sont appelées qu'une seule fois. Il permet de voir plus facilement la logique de haut niveau et peut rendre la gestion des exceptions un peu plus propre. Tant que vous n'avez pas à passer vingt paramètres pour représenter l'état de traitement!
Les méthodes longues ne sont pas toujours mauvaises. Ils indiquent généralement qu'il pourrait y avoir un problème.
Sur le système sur lequel je travaille, nous avons une demi-douzaine de méthodes de plus de 10 000 lignes. L'une est actuellement longue de 54 830 lignes. Et ça va.
Ces fonctions ridiculement longues sont très simples et sont générées automatiquement. Ce grand monstre de 54 830 lignes contient les données quotidiennes du mouvement polaire du 1er janvier 1962 au 10 janvier 2012 (notre dernière version). Nous publions également une procédure par laquelle nos utilisateurs peuvent mettre à jour ce fichier généré automatiquement. Ce fichier source contient les données de mouvement polaire de http://data.iers.org/products/214/14443/orig/eopc04_08_IAU2000.62-now , traduites automatiquement en C++.
La lecture de ce site Web à la volée n'est pas possible dans une installation sécurisée. Il n'y a aucun lien avec le monde extérieur. Le téléchargement du site Web en tant que copie locale et l'analyse en C++ n'est pas non plus une option; l'analyse est lente , et cela doit être rapide. Téléchargement, traduction automatique en C++ et compilation: vous avez maintenant quelque chose de rapide. (Il suffit de ne pas le compiler optimisé. C'est incroyable combien de temps un compilateur d'optimisation prend pour compiler 50 000 lignes de code de ligne droite extrêmement simple. Il faut plus d'une demi-heure sur mon ordinateur pour compiler ce fichier optimisé. Et l'optimisation ne fait absolument rien . Il n'y a rien à optimiser. C'est un simple code en ligne droite, une déclaration d'affectation après l'autre.)
Disons simplement qu'il existe de bonnes et de mauvaises façons de briser une longue méthode. Devoir "[garder] la méthode la plus externe dans votre tête" est un signe que vous ne la décomposez pas de la manière la plus optimale, ou que vos sous-méthodes sont mal nommées. En théorie, il existe des cas où une méthode longue est meilleure. En pratique, c'est extrêmement rare. Si vous ne savez pas comment rendre une méthode plus courte lisible, demandez à quelqu'un de réviser votre code et demandez-lui spécifiquement des idées pour raccourcir les méthodes.
En ce qui concerne plusieurs boucles provoquant un supposé impact sur les performances, il n'y a aucun moyen de le savoir sans mesurer. Plusieurs boucles plus petites peuvent être considérablement plus rapides si cela signifie que tout ce dont il a besoin peut rester dans le cache. Même s'il y a un impact sur les performances, il est généralement négligeable en faveur de la lisibilité.
Je dirai que les méthodes souvent longues sont plus faciles à écrire, même si elles sont plus difficiles à lire. C'est pourquoi ils prolifèrent même si personne ne les aime. Il n'y a rien de mal à planifier dès le départ pour refactoriser avant de vous enregistrer.
Les méthodes longues peuvent être plus efficaces en termes de calcul et d'espace, il peut être plus facile de voir la logique et plus facile de les déboguer. Cependant, ces règles ne s'appliquent vraiment que lorsqu'un seul programmeur touche ce code. Le code va être difficile à étendre s'il n'est pas atomique, essentiellement la prochaine personne devra recommencer à zéro, puis déboguer et tester cela prendra pour toujours car elle n'utilise aucun bon code connu.
Il est tout à fait correct d'écrire de longues fonctions. Mais cela varie selon le contexte, que vous en ayez vraiment besoin ou non. Par exemple certains des meilleurs algorthms s'expriment mieux quand c'est un morceau. D'un autre côté, un grand pourcentage des routines dans les programmes orientés objet seront des routines accesseurs, qui seront très courtes. Certaines des longues routines de traitement qui ont de longs commutateurs, si les conditions peuvent être optimisées via des méthodes pilotées par table.
Il y a une excellente courte discussion dans Code Complete 2 sur la longueur des routines.
La longueur maximale théorique maximale de 497 est souvent décrite comme une ou deux pages de liste de programmes, de 66 à 132 lignes. Les programmes modernes ont tendance à avoir des volumes de routines extrêmement courtes mélangés à quelques routines plus longues.
Des décennies de preuves indiquent que les routines d'une telle longueur ne sont pas plus sujettes aux erreurs que les routines plus courtes. Laissez des problèmes tels que la profondeur d'imbrication, le nombre de variables et d'autres considérations liées à la complexité dicter la longueur de la routine plutôt que d'imposer une longueur.
Si vous souhaitez écrire des routines de plus de 200 lignes environ, faites attention. Aucune des études qui ont signalé une baisse des coûts, une baisse des taux d'erreur, ou les deux avec des routines plus grandes, distinguées parmi les tailles supérieures à 200 lignes, et vous êtes obligé de rencontrer une limite supérieure de compréhensibilité lorsque vous passez 200 lignes de code. 536 restriction en soi.
Il y a quelque chose que nous appelons décomposition fonctionnelle impliquant de diviser vos méthodes plus longues en plus petites dans la mesure du possible. Comme vous avez mentionné que votre méthode implique le tri/filtrage, il vaut mieux avoir des méthodes ou des fonctions distinctes pour ces tâches.
Précisément, votre méthode doit se concentrer uniquement sur l'exécution d'une tâche.
Et s'il a besoin d'appeler une autre méthode pour une raison quelconque, faites-le sinon, continuez avec celle que vous écrivez déjà. Aussi pour des raisons de lisibilité, vous pouvez ajouter des commentaires. Classiquement, les programmeurs utilisent des commentaires sur plusieurs lignes (/ **/en C, C++, C # et Java) pour les descriptions de méthode et utilisent des commentaires sur une seule ligne (// en C, C++, C # et Java). Il existe également de bons outils de documentation pour une meilleure lisibilité du code (par exemple JavaDoc ). Vous pouvez également consulter commentaires basés sur XML si vous êtes un développeur .Net.
Les boucles ont un impact sur les performances du programme et peuvent entraîner une surcharge d'application si elles ne sont pas utilisées correctement. L'idée est de concevoir votre algorithme de manière à utiliser le moins possible de boucles imbriquées.
Je voulais citer l'exemple que vous avez donné:
Par exemple, j'ai quelques Django vues qui font un peu de traitement des objets avant de les envoyer à la vue, une longue méthode étant de 350 lignes de code. J'ai mon code écrit pour qu'il traite avec les paramètres - trier/filtrer l'ensemble de requêtes, puis peu à peu effectuer un traitement sur les objets que ma requête a renvoyés.
Dans mon entreprise, notre plus grand projet est construit sur Django et nous avons aussi des fonctions de vue longue (beaucoup sont plus de 350 lignes). Je dirais que les nôtres n'ont pas besoin d'être aussi longs, et ils nous font du mal.
Ces fonctions d'affichage effectuent de nombreux travaux vaguement liés qui doivent être extraits du modèle, des classes d'assistance ou des fonctions d'assistance. De plus, nous finissons par réutiliser des vues pour faire différentes choses, qui devraient plutôt être divisées en vues plus cohérentes.
Je soupçonne que vos opinions ont des caractéristiques similaires. Dans mon cas, je sais que cela cause des problèmes et je travaille pour apporter des changements. Si vous n'êtes pas d'accord que cela cause des problèmes, vous n'avez pas besoin de le réparer.
Encore un vote qui est presque toujours faux. Je trouve cependant deux cas de base où c'est la bonne réponse:
1) Une méthode qui appelle simplement un tas d'autres méthodes et ne fonctionne pas vraiment elle-même. Vous avez un processus qui prend 50 étapes à accomplir, vous obtenez une méthode avec 50 appels. Il n'y a généralement rien à gagner en essayant de rompre cela.
2) Répartiteurs. OOP la conception s'est débarrassée de la plupart de ces méthodes mais les sources entrantes de données sont de par leur nature même des données et ne peuvent donc pas suivre les principes OOP. Ce n'est pas exactement inhabituel d'avoir une sorte de routine de répartiteur dans le code qui gère les données.
Je dirais également qu'il ne faut même pas considérer la question lorsqu'il s'agit de choses générées automatiquement. Personne n'essaie de comprendre ce que fait le code généré automatiquement, peu importe qu'un iota soit facile à comprendre pour un humain.
Je ne sais pas si quelqu'un l'a déjà mentionné, mais l'une des raisons pour lesquelles les méthodes longues sont mauvaises est qu'elles impliquent généralement plusieurs niveaux d'abstraction. Vous avez des variables de boucle et toutes sortes de choses se produisent. Considérez la fonction fictive:
function nextSlide() {
var content = getNextSlideContent();
hideCurrentSlide();
var newSlide = createSlide(content);
setNextSlide(newSlide);
showNextSlide();
}
Si vous faisiez toute l'animation, le calcul, l'accès aux données, etc. dans cette fonction, cela aurait été un gâchis. La fonction nextSlide () conserve une couche d'abstraction cohérente (le système d'état des diapositives) et ignore les autres. Cela rend le code lisible.
Si vous devez constamment entrer dans des méthodes plus petites pour voir ce qu'elles font, alors l'exercice de division de la fonction a échoué. Ce n'est pas parce que le code que vous lisez fait des choses évidentes dans les méthodes enfants que les méthodes enfants sont une mauvaise idée, mais simplement qu'elles ont été mal faites.
Lorsque je crée des méthodes, je finis généralement par les diviser en méthodes plus petites comme une sorte de stratégie de division et de conquête. Une méthode comme
if (hasMoreRecords()) { ... }
est certainement plus lisible que
if (file.isOpen() && i < recordLimit && currentRecord != null) { ... }
Droite?
Je suis d'accord sur le fait que les déclarations absolues sont mauvaises et conviennent également qu'une méthode longue est généralement mauvaise.
Histoire vraie. J'ai rencontré une fois une méthode de plus de deux mille lignes. La méthode avait des régions qui décrivaient ce qu'elle faisait dans ces régions. Après avoir lu une région, j'ai décidé de faire une méthode d'extraction automatisée, en la nommant en fonction du nom de la région. au moment où j'ai fini, la méthode n'était rien d'autre que 40 appels de méthode d'une cinquantaine de lignes chacun et tout fonctionnait de la même manière.
Ce qui est trop grand est subjectif. Parfois, une méthode ne peut pas être décomposée plus loin qu'elle ne l'est actuellement. C'est comme écrire un livre. La plupart des gens conviennent que les longs paragraphes doivent généralement être divisés. Mais parfois, il n'y a qu'une seule idée et la diviser provoque plus de confusion que ne le ferait la longueur de ce paragraphe.
La vérité est que cela dépend. Comme mentionné, si le code ne sépare pas les problèmes et essaie de tout faire dans une même méthode, c'est un problème. La séparation du code en plusieurs modules facilite la lecture du code, ainsi que l'écriture de code (par plusieurs programmeurs). Adhérer à un module (classe) par fichier source est une bonne idée pour commencer.
Deuxièmement, en ce qui concerne les fonctions/procédures:
void setDataValueAndCheckForRange(Data *data) {/*code*/}
est une bonne méthode si elle vérifie uniquement la plage de "données". C'est une mauvaise méthode lorsque la même plage s'applique pour plusieurs fonctions (exemple de mauvais code):
void setDataValueAndCheckForRange(Data *data){ /*code */}
void addDataValuesAndCheckForRange(Data *result, Data *d1, Data *d2){ /*code*/}
void subDataValuesAndCheckForRange(Data *result, Data *d1, Data *d2){ /*code*/}
void mulDataValuesAndCheckForRange(Data *result, Data *d1, Data *d2){ /*code*/}
Cela doit être refactorisé pour:
bool isWithinRange(Data *d){ /*code*/ }
void setDataValue(Data *d) {/*code*/ if(isWithinRange(d)){/*continue*/}else{/*warn/abort*/}
void addDataValue(Data *d, Data *d1, Data *d2) {/*code*/ if(isWithinRange(d)){/*continue*/}else{/*warn/abort*/}
void subDataValue(Data *d, Data *d1, Data *d2) {/*code*/ if(isWithinRange(d)){/*continue*/}else{/*warn/abort*/}
void mulDataValue(Data *d, Data *d1, Data *d2) {/*code*/ if(isWithinRange(d)){/*continue*/}else{/*warn/abort*/}
RÉUTILISER le code autant que possible. Et c'est possible lorsque chaque fonction de votre programme est assez SIMPLE (pas nécessairement facile).
CITATION: La MONTAGNE est composée de minuscules grains de terre. L'océan est constitué de minuscules gouttes d'eau .. (- Sivananda)
Les méthodes longues tendent vers le "mauvais" dans les langages impératifs qui favorisent les déclarations, les effets secondaires et la mutabilité, précisément parce que ces fonctionnalités augmentent la complexité et donc les bogues.
Dans les langages de programmation fonctionnels, qui favorisent les expressions, la pureté et l'immuabilité, il y a moins de raisons de s'inquiéter.
Dans les langages fonctionnels et impératifs, il est toujours préférable de factoriser des morceaux de code réutilisables dans des routines de niveau supérieur communes, mais dans les langages fonctionnels qui prennent en charge la portée lexicale avec des fonctions imbriquées, etc., il est en fait préférable d'encapsuler pour masquer les sous-routines dans le haut fonction de niveau (méthode) que de les décomposer en d'autres fonctions de niveau supérieur.
Le but d'une méthode est d'aider à réduire la régurgitation du code. Une méthode doit avoir une fonction spécifique dont elle est responsable. Si vous finissez par ressasser le code à de nombreux endroits, vous courez le risque d'avoir des résultats inattendus si les spécifications que le logiciel est conçu pour répondre sont modifiées.
Pour qu'une méthode ait 350 lignes, cela suggérerait que beaucoup des tâches qu'elle exécute sont répliquées ailleurs car il est inhabituel d'exiger une telle quantité de code pour effectuer une tâche spécialisée.
Ce ne sont pas vraiment les longues méthodes qui sont une mauvaise pratique, c'est plutôt de les laisser comme ça qui est mauvais.
Je veux dire que l'acte réel de refactorisation de votre échantillon de:
varaible_1 = 0
variable_2 = 0
for object in queryset :
if object.condition_condition_a and variable_2 > 0 :
variable 1+= 1
.....
...
.
more conditions to alter the variables
return queryset, and context
à
Status status = new Status();
status.variable1 = 0;
status.variable2 = 0;
for object in queryset :
if object.condition_condition_a and status.variable2 > 0 :
status.variable1 += 1
.....
...
.
more conditions to alter the variables (status)
return queryset, and context
puis
class Status {
variable1 = 0;
variable2 = 0;
void update(object) {
if object.condition_condition_a and variable2 > 0 {
variable1 += 1
}
}
};
Status status = new Status();
for object in queryset :
status.update(object);
.....
...
.
more conditions to alter the variables (status)
return queryset, and context
vous êtes maintenant sur la bonne voie non seulement pour une méthode beaucoup plus courte, mais pour une méthode beaucoup plus utilisable et compréhensible.
Je pense que le fait que la méthode soit très longue est quelque chose à vérifier, mais certainement pas un anti-modèle instantané. La grande chose à rechercher dans les méthodes énormes est beaucoup d'imbrication. Si tu as
foreach(var x in y)
{
//do ALL the things
//....
}
et le corps de la boucle n'est pas extrêmement localisé (c'est-à-dire que vous pourriez lui envoyer moins de 4 paramètres), alors il est probablement préférable de le convertir en:
foreach(var x in y)
{
DoAllTheThings(x);
}
...
void DoAllTheThings(object x)
{
//do ALL the things
//....
}
À son tour, cela peut considérablement réduire la durée d'une fonction. Assurez-vous également de rechercher du code en double dans la fonction et de le déplacer dans une fonction distincte.
Enfin, certaines méthodes sont longues et compliquées et vous ne pouvez rien faire. Certains problèmes nécessitent des solutions qui ne se prêtent pas à un codage facile. Par exemple, l'analyse d'une grammaire très complexe peut créer de très longues méthodes auxquelles vous ne pouvez pas faire grand-chose sans l'aggraver.