Lorsque je divise de grandes méthodes (ou procédures ou fonctions - cette question n'est pas spécifique à la POO, mais comme je travaille dans les langues de POO 99% du temps, c'est la terminologie avec laquelle je suis le plus à l'aise) en beaucoup de petites , Je suis souvent mécontent des résultats. Il devient plus difficile de raisonner sur ces petites méthodes que lorsqu'elles n'étaient que des blocs de code dans la grande, car lorsque je les extrait, je perds beaucoup d'hypothèses sous-jacentes qui viennent du contexte de l'appelant.
Plus tard, quand je regarde ce code et que je vois des méthodes individuelles, je ne sais pas immédiatement d'où elles sont appelées et je les considère comme des méthodes privées ordinaires qui peuvent être appelées de n'importe où dans le fichier. Par exemple, imaginez une méthode d'initialisation (constructeur ou autre) divisée en une série de petites: dans le contexte de la méthode elle-même, vous savez clairement que l'état de l'objet est toujours invalide, mais dans une méthode privée ordinaire, vous passez probablement de l'hypothèse que cet objet est déjà initialisé et est dans un état valide.
La seule solution que j'ai vue pour cela est la where
clause de Haskell, qui vous permet de définir de petites fonctions qui ne sont utilisées que dans la fonction "parent". Fondamentalement, cela ressemble à ceci:
len x y = sqrt $ (sq x) + (sq y)
where sq a = a * a
Mais d'autres langues que j'utilise n'ont rien de tel - la chose la plus proche est de définir un lambda dans une portée locale, ce qui est probablement encore plus déroutant.
Donc, ma question est - rencontrez-vous cela, et voyez-vous même que c'est un problème? Si vous le faites, comment le résolvez-vous généralement, en particulier dans les langages de POO "grand public", comme Java / C # / C ++?
Modifier les doublons: Comme d'autres l'ont remarqué, il y a déjà des questions sur les méthodes de fractionnement et les petites questions qui sont à une ligne. Je les ai lus et ils ne discutent pas de la question des hypothèses sous - jacentes qui peuvent être dérivées du contexte de l'appelant (dans l'exemple ci-dessus, objet en cours d'initialisation). C'est le point de ma question, et c'est pourquoi ma question est différente.
Mise à jour: Si vous avez suivi cette question et cette discussion ci-dessous, vous pourriez apprécier cet article de John Carmack sur le sujet , en particulier:
Outre la connaissance du code en cours d'exécution, les fonctions en ligne ont également l'avantage de ne pas permettre d'appeler la fonction depuis d'autres endroits. Cela semble ridicule, mais il y a un point à cela. Au fur et à mesure qu'une base de code se développe au fil des années d'utilisation, il y aura de nombreuses opportunités de prendre un raccourci et d'appeler simplement une fonction qui ne fait que le travail qui, selon vous, doit être fait. Il peut y avoir une fonction FullUpdate () qui appelle PartialUpdateA () et PartialUpdateB (), mais dans certains cas particuliers, vous pouvez réaliser (ou penser) que vous n'avez besoin que de faire PartialUpdateB (), et vous êtes efficace en évitant les autres travail. Beaucoup, beaucoup de bugs en découlent. La plupart des bogues sont dus au fait que l'état d'exécution n'est pas exactement ce que vous pensez qu'il est.
Réponses:
Votre préoccupation est fondée. Il y a une autre solution.
Prendre du recul. À quoi sert fondamentalement une méthode? Les méthodes ne font qu'une ou deux choses:
Ou, malheureusement, les deux. J'essaie d'éviter les méthodes qui font les deux, mais beaucoup le font. Disons que l'effet produit ou la valeur produite est le "résultat" de la méthode.
Vous notez que les méthodes sont appelées dans un "contexte". Quel est ce contexte?
Ce que vous signalez essentiellement, c'est que l'exactitude du résultat de la méthode dépend du contexte dans lequel elle est appelée .
Nous appelons les conditions requises avant qu'un corps de méthode commence pour que la méthode produise un résultat correct ses conditions préalables , et nous appelons les conditions qui seront produites après que le corps de méthode retourne ses postconditions .
Donc, essentiellement, ce que vous signalez est: lorsque j'extrais un bloc de code dans sa propre méthode, je perds des informations contextuelles sur les conditions préalables et postconditions .
La solution à ce problème est de rendre explicites les conditions préalables et postconditions dans le programme . En C #, par exemple, vous pouvez utiliser
Debug.Assert
ou Code Contracts pour exprimer les conditions préalables et postconditions.Par exemple: je travaillais sur un compilateur qui passait par plusieurs "étapes" de compilation. D'abord, le code serait lexiqué, puis analysé, puis les types seraient résolus, puis les hiérarchies d'héritage seraient vérifiées pour les cycles, etc. Chaque morceau du code était très sensible à son contexte; il serait désastreux, par exemple, de demander "ce type est-il convertible en ce type?" si le graphique des types de base n'était pas encore connu pour être acyclique! Par conséquent, chaque bit de code a clairement documenté ses conditions préalables. Nous
assert
dans la méthode pour la convertibilité du type que nous avions déjà passé vérifié le chèque « types de base acyclique », et il est ensuite devenu clair pour le lecteur où la méthode pourrait être appelé et où il ne pouvait pas être appelé.Bien sûr, il existe de nombreuses façons dont une bonne conception de méthode atténue le problème que vous avez identifié:
la source
string
et l'enregistre dans la base de données, vous risquez une injection SQL si vous oubliez de la nettoyer. Si, d'autre part, votre fonction prend unSanitisedString
, et la seule façon d'obtenir unSantisiedString
est en appelantSanitise
, alors vous avez exclu les bogues d'injection SQL par construction. Je me retrouve de plus en plus à chercher des moyens pour que le compilateur rejette le code incorrect.Je vois souvent cela et je reconnais que c'est un problème. Habituellement, je le résous en créant un objet méthode : une nouvelle classe spécialisée dont les membres sont les variables locales de la méthode originale, trop volumineuse.
La nouvelle classe a tendance à avoir un nom comme «Exportateur» ou «Tabulation», et elle obtient toutes les informations nécessaires pour effectuer cette tâche particulière dans un contexte plus large. Ensuite, il est libre de définir des extraits de code d'assistance encore plus petits qui ne risquent pas d'être utilisés pour autre chose que la tabulation ou l'exportation.
la source
De nombreuses langues vous permettent d'imbriquer des fonctions comme Haskell. Java / C # / C ++ sont en fait des valeurs aberrantes relatives à cet égard. Malheureusement, ils sont si populaires que les gens en viennent à penser: "Ce doit être une mauvaise idée, sinon ma langue" dominante "préférée le permettrait."
Java / C # / C ++ pense fondamentalement qu'une classe devrait être le seul regroupement de méthodes dont vous ayez jamais besoin. Si vous avez tellement de méthodes que vous ne pouvez pas déterminer leurs contextes, il y a deux approches générales à suivre: les trier par contexte ou les diviser par contexte.
Le tri par contexte est une recommandation faite dans Clean Code , où l'auteur décrit un modèle de «paragraphes TO». Il s'agit essentiellement de placer vos fonctions d'assistance immédiatement après la fonction qui les appelle, de sorte que vous pouvez les lire comme des paragraphes dans un article de journal, obtenir plus de détails plus vous lisez. Je pense que dans ses vidéos, il les met même en retrait.
L'autre approche consiste à diviser vos classes. Cela ne peut pas être poussé très loin, en raison du besoin ennuyeux d'instancier des objets avant de pouvoir appeler des méthodes dessus, et des problèmes inhérents à décider laquelle de plusieurs petites classes devrait posséder chaque élément de données. Cependant, si vous avez déjà identifié plusieurs méthodes qui ne rentrent vraiment que dans un seul contexte, elles sont probablement un bon candidat à envisager de mettre dans leur propre classe. Par exemple, une initialisation complexe peut être effectuée dans un modèle de création comme le générateur.
la source
Je pense que la réponse dans la plupart des cas est le contexte. En tant que développeur qui écrit du code, vous devez supposer que votre code sera modifié à l'avenir. Une classe peut être intégrée à une autre classe, remplacer son algorithme interne ou être divisée en plusieurs classes afin de créer une abstraction. Ce sont des choses que les développeurs débutants ne prennent généralement pas en compte, ce qui entraîne un besoin de solutions de contournement compliquées ou de révisions complètes plus tard.
L'extraction des méthodes est bonne, mais dans une certaine mesure. J'essaie toujours de me poser ces questions lors de l'inspection ou avant d'écrire du code:
Dans tous les cas, pensez toujours à une seule responsabilité. Une classe devrait avoir une responsabilité, ses fonctions devraient servir un seul service constant, et si elles effectuent un certain nombre d'actions, ces actions devraient avoir leurs propres fonctions, il est donc facile de les différencier ou de les changer plus tard.
la source
Je ne me suis pas rendu compte de l'ampleur du problème jusqu'à ce que j'adopte un ECS qui encourage les fonctions système plus grandes et en boucle (les systèmes étant les seuls à avoir des fonctions) et les dépendances qui se dirigent vers les données brutes , pas les abstractions.
À ma grande surprise, cela a produit une base de code tellement plus facile à raisonner et à maintenir par rapport aux bases de code dans lesquelles j'ai travaillé dans le passé où, pendant le débogage, vous deviez tracer toutes sortes de petites fonctions minuscules, souvent par le biais d'appels de fonctions abstraites via des interfaces pures menant à qui sait où jusqu'à ce que vous y traçiez, seulement pour engendrer une cascade d'événements qui mènent à des endroits que vous n'auriez jamais pensé que le code devrait jamais mener.
Contrairement à John Carmack, mon plus gros problème avec ces bases de code n'était pas les performances, car je n'avais jamais eu cette demande de latence ultra-serrée des moteurs de jeu AAA et la plupart de nos problèmes de performances concernaient davantage le débit. Bien sûr, vous pouvez également commencer à rendre de plus en plus difficile l'optimisation des hotspots lorsque vous travaillez dans des limites de plus en plus étroites de fonctions et de classes pour adolescents et adolescents sans que cette structure ne vous gêne (vous obligeant à fusionner toutes ces petites pièces) à quelque chose de plus grand avant que vous puissiez même commencer à y faire face efficacement).
Pourtant, le plus gros problème pour moi était de ne pas pouvoir raisonner en toute confiance sur l'exactitude globale du système malgré tous les tests réussis. Il y avait trop de choses à prendre dans mon cerveau et à comprendre parce que ce type de système ne vous permettait pas de le raisonner sans prendre en compte tous ces petits détails et les interactions sans fin entre de minuscules fonctions et des objets qui se passaient partout. Il y avait trop de «et si?», Trop de choses qui devaient être appelées au bon moment, trop de questions sur ce qui se passerait si elles étaient appelées au mauvais moment (qui commencent à devenir soulevées au point de paranoïa lorsque vous avoir un événement déclenchant un autre événement déclenchant un autre vous menant à toutes sortes de lieux imprévisibles), etc.
Maintenant, j'aime mes fonctions de 80 lignes de gros cul ici et là, tant qu'elles exercent toujours une responsabilité singulière et claire et n'ont pas comme 8 niveaux de blocs imbriqués. Ils donnent l'impression qu'il y a moins de choses dans le système à tester et à comprendre, même si les versions plus petites et découpées de ces fonctions plus importantes n'étaient que des détails d'implémentation privés ne pouvant être appelés par personne d'autre ... on a tendance à penser qu'il y a moins d'interactions dans tout le système. J'aime même une duplication de code très modeste, tant que ce n'est pas une logique complexe (disons juste 2-3 lignes de code), si cela signifie moins de fonctions. J'aime le raisonnement de Carmack à propos de l'inclusion de rendre cette fonctionnalité impossible à appeler ailleurs dans le fichier source. Là'
La simplicité ne réduit pas toujours la complexité au niveau global si l'option est entre une fonction charnue et 12 fonctions ultra-simples qui s'appellent les unes les autres avec un graphique complexe de dépendances. À la fin de la journée, vous devez souvent raisonner sur ce qui se passe au-delà d'une fonction, sur ce que ces fonctions totalisent pour finalement faire, et il peut être plus difficile de voir cette vue d'ensemble si vous devez la déduire de la les plus petites pièces du puzzle.
Bien sûr, le code de type bibliothèque très polyvalent qui est bien testé peut être exempté de cette règle, car ce code polyvalent fonctionne souvent bien et se suffit à lui-même. De plus, il a tendance à être minuscule par rapport au code un peu plus proche du domaine de votre application (des milliers de lignes de code, pas des millions), et si largement applicable qu'il commence à faire partie du vocabulaire quotidien. Mais avec quelque chose de plus spécifique à votre application où les invariants à l'échelle du système que vous devez maintenir vont bien au-delà d'une seule fonction ou classe, j'ai tendance à trouver utile d'avoir des fonctions plus charnues pour une raison quelconque. Je trouve qu'il est beaucoup plus facile de travailler avec des pièces de puzzle plus grandes en essayant de comprendre ce qui se passe avec la grande image.
la source
Je ne pense pas que ce soit un gros problème, mais je conviens que c'est gênant. Habituellement, je place juste l'assistant juste après son bénéficiaire et j'ajoute un suffixe "Helper". Cela plus le
private
spécificateur d'accès devraient clarifier son rôle. S'il y a un invariant qui ne tient pas lorsque l'aide est appelée, j'ajoute un commentaire dans l'aide.Cette solution a l'inconvénient malheureux de ne pas saisir l'étendue de la fonction qu'elle aide. Idéalement, vos fonctions sont petites, donc j'espère que cela n'entraînera pas trop de paramètres. Normalement, vous résoudre ce problème en définissant de nouvelles structures ou classes pour regrouper les paramètres, mais la quantité de passe-partout requise pour cela peut facilement être plus longue que l'aide elle-même, puis vous êtes de retour où vous avez commencé sans aucun moyen évident d'associer la structure avec la fonction.
Vous avez déjà mentionné l'autre solution - définir l'aide à l'intérieur de la fonction principale. C'est peut-être un idiome quelque peu inhabituel dans certaines langues, mais je ne pense pas que ce serait déroutant (à moins que vos pairs ne soient confondus par les lambdas en général). Cela ne fonctionne que si vous pouvez définir facilement des fonctions ou des objets similaires. Je n'essaierais pas cela en Java 7, par exemple, car une classe anonyme nécessite l'introduction de 2 niveaux d'imbrication, même pour la plus petite "fonction". Cela se rapproche le plus possible d'une clause
let
orwhere
; vous pouvez vous référer aux variables locales avant la définition et l'assistant ne peut pas être utilisé en dehors de cette portée.la source