Fonctions qui appellent uniquement d'autres fonctions. Est-ce une bonne pratique?

13

Je travaille actuellement sur un ensemble de rapports qui ont de nombreuses sections différentes (toutes nécessitant une mise en forme différente), et j'essaie de trouver la meilleure façon de structurer mon code. Des rapports similaires que nous avons réalisés dans le passé finissent par avoir de très grandes fonctions (200+ lignes) qui effectuent toutes les manipulations et le formatage des données pour le rapport, de sorte que le flux de travail ressemble à ceci:

DataTable reportTable = new DataTable();
void RunReport()
{
    reportTable = DataClass.getReportData();
    largeReportProcessingFunction();
    outputReportToUser();
}

J'aimerais pouvoir diviser ces grandes fonctions en morceaux plus petits, mais je crains de ne finir par avoir des dizaines de fonctions non réutilisables, et une fonction similaire "tout faire ici" dont le seul travail consiste à appeler toutes ces petites fonctions, comme ceci:

void largeReportProcessingFunction()
{
    processSection1HeaderData();
    calculateSection1HeaderAverages();
    formatSection1HeaderDisplay();
    processSection1SummaryTableData();
    calculateSection1SummaryTableTotalRow();
    formatSection1SummaryTableDisplay();
    processSection1FooterData();
    getSection1FooterSummaryTotals();
    formatSection1FooterDisplay();

    processSection2HeaderData();
    calculateSection1HeaderAverages();
    formatSection1HeaderDisplay();
    calculateSection1HeaderAverages();
    ...
}

Ou, si nous allons plus loin:

void largeReportProcessingFunction()
{
    callAllSection1Functions();

    callAllSection2Functions();

    callAllSection3Functions();
    ...        
}

Est-ce vraiment une meilleure solution? D'un point de vue organisationnel, je suppose que c'est le cas (c'est-à-dire que tout est beaucoup plus organisé qu'il ne le serait autrement), mais en ce qui concerne la lisibilité du code, je ne suis pas sûr (potentiellement de grandes chaînes de fonctions qui n'appellent que d'autres fonctions).

Pensées?

Eric C.
la source
En fin de compte ... est-ce plus lisible? Oui, c'est donc une meilleure solution.
sylvanaar

Réponses:

18

Ceci est communément appelé décomposition fonctionnelle et est généralement une bonne chose à faire, s'il est fait correctement.

De plus, l'implémentation d'une fonction doit se faire dans un seul niveau d'abstraction. Si vous le faites largeReportProcessingFunction, son rôle est de définir quelles différentes étapes de traitement doivent être prises dans quel ordre. La mise en œuvre de chacune de ces étapes se fait sur une couche d'abstraction ci-dessous et largeReportProcessingFunctionne devrait pas en dépendre directement.

Veuillez noter que c'est cependant un mauvais choix de nom:

void largeReportProcessingFunction() {
    callAllSection1Functions();
    callAllSection2Functions();
    callAllSection3Functions();
    ...        
}

Vous voyez, callAllSection1Functionsc'est un nom qui ne fournit pas réellement une abstraction, car il ne dit pas réellement ce qu'il fait, mais plutôt comment il le fait. Il doit être appelé à la processSection1place ou tout ce qui est réellement significatif dans le contexte.

back2dos
la source
6
Je suis d'accord en principe avec cette réponse, sauf que je ne vois pas comment "processSection1" est un nom significatif. C'est pratiquement équivalent à "callAllSection1Functions".
Eric King
2
@EricKing: divulgue des callAllSection1Functionsdétails sur la mise en œuvre. Du point de vue extérieur, peu importe que processSection1son travail soit reporté à "toutes les fonctions de la section 1" ou qu'il le fasse directement ou qu'il utilise de la poudre de lutin pour invoquer une licorne qui fera le travail réel. Tout ce qui compte vraiment, c'est qu'après un appel à processSection1, la section 1 puisse être considérée comme traitée . Je conviens que le traitement est un nom générique, mais si vous avez une cohésion élevée (que vous devriez toujours rechercher), votre contexte est généralement suffisamment étroit pour le lever.
back2dos
2
Je voulais simplement dire que les méthodes appelées "processXXX" sont presque toujours des noms terribles et inutiles. Toutes les méthodes «traitent» les choses, donc le nommer «processXXX» est à peu près aussi utile que le nommer «doSomethingWithXXX» ou «manipulateXXX» et similaire. Il existe presque toujours un meilleur nom pour les méthodes nommées «processXXX». D'un point de vue pratique, c'est aussi utile (inutile) que 'callAllSection1Functions'.
Eric King
4

Vous avez dit que vous utilisez C #, alors je me demande si vous pourriez construire une hiérarchie de classe structurée en tirant parti du fait que vous utilisez un langage orienté objet? Par exemple, s'il est judicieux de diviser le rapport en sections, créez une Sectionclasse et étendez-la pour les besoins spécifiques du client.

Il peut être judicieux d'y penser comme si vous créiez une interface graphique non interactive. Pensez aux objets que vous pourriez créer comme s'il s'agissait de différents composants de l'interface graphique. Demandez-vous à quoi ressemblerait un rapport de base? Que diriez-vous d'un complexe? Où devraient être les points d'extension?

Jeremy Heiler
la source
3

Ça dépend. Si toutes les fonctions que vous créez sont vraiment une seule unité de travail, c'est bien, si ce ne sont que les lignes 1-15, 16-30, 31-45 de leur fonction d'appel qui est mauvaise. Les fonctions longues ne sont pas mauvaises, si elles font une chose, si vous avez 20 filtres pour votre rapport ayant une fonction de validation qui vérifie les vingt va être longue. C'est très bien, le séparer par filtre ou groupes de filtres ajoute simplement de la complexité supplémentaire sans réel avantage.

Le fait d'avoir beaucoup de fonctions privées rend également les tests unitaires automatisés plus difficiles, donc certains essaieront de l'éviter pour cette raison, mais c'est un raisonnement plutôt médiocre à mon avis car cela tend à créer une conception plus grande et plus complexe pour accueillir les tests.

Ryathal
la source
3
D'après mon expérience, les fonctions longues sont mauvaises.
Bernard
Si votre exemple était en langage OO, je créerais une classe de filtre de base et chacun des 20 filtres serait dans des classes dérivées différentes. L'instruction switch serait remplacée par un appel de création pour obtenir le bon filtre. Chaque fonction fait une chose et toutes sont petites.
DXM
3

Puisque vous utilisez C #, essayez de penser davantage aux objets. Il semble que vous codiez toujours de manière procédurale en ayant des variables de classe "globales" dont dépendent les fonctions suivantes.

Décomposer votre logique en fonctions séparées est certainement mieux que d'avoir toute la logique dans une seule fonction. Je parierais que vous pourriez aller plus loin en séparant également les données sur lesquelles les fonctions fonctionnent en les répartissant en plusieurs objets.

Envisagez d'avoir une classe ReportBuilder où vous pouvez transmettre les données du rapport. Si vous pouvez diviser ReportBuilder en classes distinctes, faites-le également.

codeur
la source
2
Les procédures remplissent parfaitement leur fonction.
DeadMG
1

Oui.

Si vous avez un segment de code qui doit être utilisé à plusieurs endroits, sa propre fonction. Sinon, si vous devez modifier la fonctionnalité, vous devrez effectuer la modification à plusieurs endroits. Cela augmente non seulement le travail, mais augmente également le risque de voir apparaître des bogues lorsque le code est modifié à un endroit mais pas à un autre ou là où un mais est introduit à un endroit mais pas à l'autre.

Il aide également à rendre la méthode plus lisible si des noms de méthode descriptifs sont utilisés.

SoylentGray
la source
1

Le fait que vous utilisez la numérotation pour distinguer les fonctions suggère qu'il existe des problèmes sous-jacents avec la duplication ou une classe en faisant trop. La division d'une grande fonction en plusieurs fonctions plus petites peut être une étape utile pour refactoriser la duplication, mais ce ne devrait pas être la dernière. Par exemple, vous créez les deux fonctions:

processSection1HeaderData();
processSection2HeaderData();

N'y a-t-il pas des similitudes dans le code utilisé pour traiter les en-têtes de section? Pouvez-vous extraire une fonction commune pour les deux en paramétrant les différences?

Garrett Hall
la source
Ce n'est pas réellement du code de production, donc les noms de fonction ne sont que des exemples (en production, les noms de fonction sont plus descriptifs). En règle générale, non, il n'y a pas de similitudes entre les différentes sections (bien qu'en cas de similitudes, j'essaie de réutiliser le code autant que possible).
Eric C.
1

La décomposition d'une fonction en sous-fonctions logiques est utile, même si les sous-fonctions ne sont pas réutilisées - elle la décompose en blocs courts et faciles à comprendre. Mais cela doit être fait par des unités logiques, pas seulement n # de lignes. La façon dont vous devez le diviser dépendra de votre code, les thèmes courants seraient les boucles, les conditions, les opérations sur le même objet; essentiellement tout ce que vous pouvez décrire comme une tâche discrète.

Donc, oui, c'est un bon style - cela peut sembler étrange, mais étant donné que vous décrivez une réutilisation limitée, je vous recommande de réfléchir attentivement à la réutilisation ATTEMPTING. Assurez-vous que l'utilisation est vraiment identique, et pas seulement la même coïncidence. Tenter de gérer tous les cas dans le même bloc est souvent la façon dont vous vous retrouvez avec la grande fonction disgracieuse en premier lieu.

jmoreno
la source
0

Je pense que c'est surtout une préférence personnelle. Si vos fonctions devaient être réutilisées (et êtes-vous sûr que vous ne pourriez pas les rendre plus génériques et réutilisables?) Je dirais certainement de les diviser. J'ai également entendu dire que c'est un bon principe qu'aucune fonction ou méthode ne devrait contenir plus de 2 à 3 écrans de code. Donc, si votre grande fonction continue encore et encore, cela pourrait rendre votre code plus lisible pour le diviser.

Vous ne mentionnez pas la technologie que vous utilisez pour développer ces rapports. Il semble que vous utilisiez C #. Est-ce exact? Si c'est le cas, vous pouvez également considérer la directive #region comme une alternative si vous ne voulez pas diviser votre fonction en plus petites.

Joshua Carmody
la source
Oui, je travaille en C #. Il est possible que je puisse réutiliser certaines fonctions, mais pour beaucoup de ces rapports, les différentes sections ont des données et des dispositions très différentes (exigence du client).
Eric C.
1
+1 sauf pour les régions suggérées. Je n'utiliserais pas de régions.
Bernard
@Bernard - Une raison particulière? Je suppose que le demandeur n'envisagerait d'utiliser #regions que s'il avait déjà exclu de diviser la fonction.
Joshua Carmody
2
Les régions ont tendance à masquer le code et peuvent être utilisées abusivement (c'est-à-dire les régions imbriquées). J'aime voir tout le code à la fois dans un fichier de code. Si je dois séparer visuellement du code dans un fichier, j'utilise une ligne de barres obliques.
Bernard
2
Les régions sont généralement un signe que le code doit être refactorisé
codeur