web-dev-qa-db-fra.com

Lisibilité versus maintenabilité, cas particulier de l'écriture d'appels de fonction imbriqués

Mon style de codage pour les appels de fonction imbriqués est le suivant:

var result_h1 = H1(b1);
var result_h2 = H2(b2);
var result_g1 = G1(result_h1, result_h2);
var result_g2 = G2(c1);
var a = F(result_g1, result_g2);

J'ai récemment changé pour un département où le style de codage suivant est très utilisé:

var a = F(G1(H1(b1), H2(b2)), G2(c1));

Le résultat de ma façon de coder est que, en cas de panne, Visual Studio peut ouvrir le vidage correspondant et indiquer la ligne où le problème se produit (je suis particulièrement préoccupé par les violations d'accès).

Je crains qu'en cas de crash dû au même problème programmé en premier, je ne puisse pas savoir quelle fonction a provoqué le crash.

D'un autre côté, plus vous mettez de traitement sur une ligne, plus vous obtenez de logique sur une page, ce qui améliore la lisibilité.

Ma peur est-elle correcte ou manque-t-il quelque chose, et en général, qui est préféré dans un environnement commercial? Lisibilité ou maintenabilité?

Je ne sais pas si c'est pertinent, mais nous travaillons en C++ (STL)/C #.

57
Dominique

Si vous vous sentiez obligé d'étendre une doublure comme

 a = F(G1(H1(b1), H2(b2)), G2(c1));

Je ne te blâmerais pas. Ce n'est pas seulement difficile à lire, c'est difficile à déboguer.

Pourquoi?

  1. C'est dense
  2. Certains débogueurs ne mettent en évidence le tout qu'à la fois
  3. Il est exempt de noms descriptifs

Si vous le développez avec des résultats intermédiaires, vous obtenez

 var result_h1 = H1(b1);
 var result_h2 = H2(b2);
 var result_g1 = G1(result_h1, result_h2);
 var result_g2 = G2(c1);
 var a = F(result_g1, result_g2);

et c'est encore difficile à lire. Pourquoi? Il résout deux des problèmes et en introduit un quatrième:

  1. C'est dense
  2. Certains débogueurs ne mettent en évidence le tout qu'à la fois
  3. Il est exempt de noms descriptifs
  4. Il est encombré de noms non descriptifs

Si vous le développez avec des noms qui ajoutent une nouvelle signification sémantique, c'est encore mieux! Un bon nom m'aide à comprendre.

 var temperature = H1(b1);
 var humidity = H2(b2);
 var precipitation = G1(temperature, humidity);
 var dewPoint = G2(c1);
 var forecast = F(precipitation, dewPoint);

Maintenant, au moins, cela raconte une histoire. Il résout les problèmes et est clairement meilleur que tout ce qui est proposé ici, mais il vous oblige à trouver les noms.

Si vous le faites avec des noms sans signification comme result_this Et result_that Parce que vous ne pouvez tout simplement pas penser à de bons noms, alors je préférerais vraiment que vous nous épargniez l'encombrement de noms sans signification et que vous l'agrandissiez en utilisant de bons ancien espace blanc:

int a = 
    F(
        G1(
            H1(b1), 
            H2(b2)
        ), 
        G2(c1)
    )
;

Il est tout aussi lisible, sinon plus, que celui avec les noms de résultats sans signification (pas que ces noms de fonction soient si bons).

  1. C'est dense
  2. Certains débogueurs ne mettent en évidence le tout qu'à la fois
  3. Il est exempt de noms descriptifs
  4. Il est encombré de noms non descriptifs

Quand vous ne pouvez pas penser à de bons noms, c'est aussi bien que possible.

Pour une raison quelconque les débogueurs adorent les nouvelles lignes vous devriez donc trouver que le débogage n'est pas difficile:

enter image description here

Si cela ne suffit pas, imaginez que G2() a été appelée à plusieurs endroits et que cela s'est produit:

Exception in thread "main" Java.lang.NullPointerException
    at composition.Example.G2(Example.Java:34)
    at composition.Example.main(Example.Java:18)

Je pense que c'est bien que puisque chaque appel de G2() soit sur sa propre ligne, ce style vous amène directement à l'appel incriminé en principal.

Donc, s'il vous plaît, n'utilisez pas les problèmes 1 et 2 comme excuse pour nous coller au problème 4. Utilisez de bons noms quand vous pouvez y penser. Évitez les noms dénués de sens lorsque vous ne le pouvez pas.

Les courses de légèreté dans Orbit's commentaire indiquent correctement que ces fonctions sont artificielles et ont elles-mêmes des noms pauvres. Voici donc un exemple d'application de ce style à du code sauvage:

var user = db.t_ST_User.Where(_user => string.Compare(domain,  
_user.domainName.Trim(), StringComparison.OrdinalIgnoreCase) == 0)
.Where(_user => string.Compare(samAccountName, _user.samAccountName.Trim(), 
StringComparison.OrdinalIgnoreCase) == 0).Where(_user => _user.deleted == false)
.FirstOrDefault();

Je déteste regarder ce flux de bruit, même lorsque l'habillage Word n'est pas nécessaire. Voici à quoi cela ressemble sous ce style:

var user = db
    .t_ST_User
    .Where(
        _user => string.Compare(
            domain, 
            _user.domainName.Trim(), 
            StringComparison.OrdinalIgnoreCase
        ) == 0
    )
    .Where(
        _user => string.Compare(
            samAccountName, 
            _user.samAccountName.Trim(), 
            StringComparison.OrdinalIgnoreCase
        ) == 0
    )
    .Where(_user => _user.deleted == false)
    .FirstOrDefault()
;

Comme vous pouvez le voir, j'ai trouvé que ce style fonctionne bien avec le code fonctionnel qui se déplace dans l'espace orienté objet. Si vous pouvez trouver de bons noms pour le faire dans un style intermédiaire, alors plus de pouvoir pour vous. Jusque-là, j'utilise cela. Mais dans tous les cas, veuillez trouver un moyen d'éviter les noms de résultats sans signification. Ils me font mal aux yeux.

111
candied_orange

D'un autre côté, plus vous mettez de traitement sur une ligne, plus vous obtenez de logique sur une page, ce qui améliore la lisibilité.

Je suis totalement en désaccord avec cela. Le simple fait de regarder vos deux exemples de code appelle cela comme incorrect:

var a = F(G1(H1(b1), H2(b2)), G2(c1));

s'entend lire. La "lisibilité" ne signifie pas la densité de l'information; cela signifie "facile à lire, à comprendre et à entretenir".

Parfois, le code est simple et il est logique d'utiliser une seule ligne. D'autres fois, cela rend la lecture plus difficile, sans aucun avantage évident au-delà de l'entassement de plus sur une seule ligne.

Cependant, je vous appellerais également en affirmant que "facile à diagnostiquer les plantages" signifie que le code est facile à maintenir. Le code qui ne plante pas est beaucoup plus facile à maintenir. "Facile à entretenir" est obtenu principalement via le code facile à lire et à comprendre, soutenu par un bon ensemble de tests automatisés.

Donc, si vous transformez une expression unique en une expression multiligne avec de nombreuses variables simplement parce que votre code se bloque souvent et que vous avez besoin de meilleures informations de débogage, arrêtez de le faire et rendez le code plus robuste à la place. Vous devriez préférer écrire du code qui n'a pas besoin d'être débogué plutôt que du code facile à déboguer.

50
David Arno

Votre premier exemple, le formulaire d'attribution unique, est illisible car les noms choisis sont totalement dénués de sens. Cela pourrait être un artefact d'essayer de ne pas divulguer des informations internes de votre part, le vrai code pourrait être bien à cet égard, nous ne pouvons pas le dire. Quoi qu'il en soit, il est de longue haleine en raison d'une densité d'informations extrêmement faible, ce qui ne se prête généralement pas à une compréhension facile.

Votre deuxième exemple est condensé à un degré absurde. Si les fonctions avaient des noms utiles, cela pourrait être bien et bien lisible car il n'y en a pas , mais en l'état, c'est déroutant dans le autre direction.

Après avoir introduit des noms significatifs, vous pouvez vous demander si l'une des formes semble naturelle, ou si il y a un milieu doré à rechercher.

Maintenant que vous avez du code lisible, la plupart des bogues seront évidents et les autres auront au moins plus de mal à vous cacher.

25
Deduplicator

Comme toujours, en matière de lisibilité, l'échec est dans les extrêmes . Vous pouvez prendre any de bons conseils de programmation, en faire une règle religieuse et l'utiliser pour produire du code totalement illisible. (Si vous ne me croyez pas à ce sujet, consultez ces deux IOCCC gagnants borsanyi et goren = et regardez comment ils utilisent différemment les fonctions pour rendre le code totalement illisible. Astuce: Borsanyi utilise exactement une fonction, beaucoup plus, bien plus ...)

Dans votre cas, les deux extrêmes sont 1) en utilisant uniquement des instructions d'expression unique, et 2) en joignant le tout en instructions grandes, laconiques et complexes. L'une ou l'autre approche prise à l'extrême rend votre code illisible.

Votre tâche, en tant que programmeur, est de trouver un équilibre . Pour chaque instruction que vous écrivez, il est votre tâche de répondre à la question: "Cette instruction est-elle facile à saisir et sert-elle à rendre ma fonction lisible?"


Le fait est qu'il n'y a pas de complexité d'un seul énoncé mesurable qui puisse décider de ce qui est bon à inclure dans un seul énoncé. Prenons par exemple la ligne:

double d = sqrt(square(x1 - x0) + square(y1 - y0));

C'est une déclaration assez complexe, mais tout programmeur digne de ce nom devrait être capable de saisir immédiatement ce que cela fait. C'est un schéma assez connu. En tant que tel, il est beaucoup plus lisible que son équivalent

double dx = x1 - x0;
double dy = y1 - y0;
double dxSquare = square(dx);
double dySquare = square(dy);
double dSquare = dxSquare + dySquare;
double d = sqrt(dSquare);

ce qui brise le modèle bien connu en un nombre apparemment sans signification d'étapes simples. Cependant, la déclaration de votre question

var a = F(G1(H1(b1), H2(b2)), G2(c1));

me semble trop compliqué, même si c'est une opération de moins que le calcul de la distance . Bien sûr, c'est une conséquence directe du fait que je ne sache rien de F(), G1(), G2(), H1() ou H2(). Je pourrais décider différemment si j'en savais plus à leur sujet. Mais c'est précisément le problème: la complexité souhaitable d'une déclaration dépend fortement du contexte et des opérations impliquées. Et vous, en tant que programmeur, êtes celui qui doit examiner ce contexte et décider quoi inclure dans une seule déclaration. Si vous vous souciez de la lisibilité, vous ne pouvez pas décharger cette responsabilité sur une règle statique.

17

@ Dominique, je pense que dans l'analyse de votre question, vous faites l'erreur que "lisibilité" et "maintenabilité" sont deux choses distinctes.

Est-il possible d'avoir un code maintenable mais illisible? Inversement, si le code est extrêmement lisible, pourquoi deviendrait-il impossible à maintenir en raison de sa lisibilité? Je n'ai jamais entendu parler d'un programmeur qui aurait joué ces facteurs les uns contre les autres, devant choisir l'un ou l'autre!

En ce qui concerne la décision d'utiliser ou non des variables intermédiaires pour les appels de fonctions imbriquées, dans le cas de 3 variables données, d'appels à 5 fonctions distinctes et de certains appels imbriqués à 3 profonds, j'aurais tendance à utiliser au moins certains variables intermédiaires pour décomposer cela, comme vous l'avez fait.

Mais je ne vais certainement pas jusqu'à dire que les appels de fonction ne doivent jamais être imbriqués. C'est une question de jugement dans les circonstances.

Je dirais que les points suivants portent sur le jugement:

  1. Si les fonctions appelées représentent des opérations mathématiques standard, elles sont plus susceptibles d'être imbriquées que les fonctions qui représentent une logique de domaine obscure dont les résultats sont imprévisibles et ne peuvent pas nécessairement être évalués mentalement par le lecteur.

  2. Une fonction avec un seul paramètre est plus capable de participer à un nid (en tant que fonction interne ou externe) qu'une fonction avec plusieurs paramètres. Le mélange des fonctions de différentes arités à différents niveaux d'imbrication a tendance à laisser le code ressembler à l'oreille d'un porc.

  3. Un nid de fonctions que les programmeurs sont habitués à voir exprimées d'une manière particulière - peut-être parce qu'elles représentent une technique ou une équation mathématique standard, qui a une implémentation standard - peut être plus difficile à lire et à vérifier si elle est décomposé en variables intermédiaires.

  4. Un petit nid d'appels de fonction qui exécute une fonctionnalité simple et est déjà clair à lire, puis est décomposé excessivement et atomisé, est capable d'être plus difficile à lire qu'un autre qui n'a pas été décomposé du tout .

14
Steve

Les deux sont sous-optimaux. Tenez compte des commentaires.

// Calculating torque according to Newton/Dominique, 4th ed. pg 235
var a = F(G1(H1(b1), H2(b2)), G2(c1));

Ou des fonctions spécifiques plutôt que générales:

var a = Torque_NewtonDominique(b1,b2,c1);

Lorsque vous décidez des résultats à énoncer, gardez à l'esprit le coût (copie vs référence, valeur l vs valeur r), la lisibilité et le risque, individuellement pour chaque déclaration.

Par exemple, il n'y a aucune valeur ajoutée à déplacer des conversions simples d'unité/type sur leurs propres lignes, car elles sont faciles à lire et extrêmement peu susceptibles d'échouer:

var radians = ExtractAngle(c1.Normalize())
var a = Torque(b1.ToNewton(),b2.ToMeters(),radians);

En ce qui concerne votre souci d'analyser les vidages sur incident, la validation des entrées est généralement beaucoup plus importante - le crash réel est très susceptible de se produire à l'intérieur de ces fonctions plutôt que sur la ligne qui les appelle, et même si ce n'est pas le cas, vous n'avez généralement pas besoin de savoir exactement où les choses ont explosé. Il est beaucoup plus important de savoir où les choses ont commencé à s'effondrer, que de savoir où elles ont finalement explosé, ce qui attrape la validation des entrées.

4
Peter

À mon avis, le code auto-documenté est meilleur pour la maintenabilité et la lisibilité, quelle que soit la langue.

La déclaration donnée ci-dessus est dense, mais "auto-documentée":

double d = sqrt(square(x1 - x0) + square(y1 - y0));

Lorsqu'il est divisé en étapes (plus faciles à tester, sûrement), il perd tout le contexte comme indiqué ci-dessus:

double dx = x1 - x0;
double dy = y1 - y0;
double dxSquare = square(dx);
double dySquare = square(dy);
double dSquare = dxSquare + dySquare;
double d = sqrt(dSquare);

Et évidemment, l'utilisation de noms de variables et de fonctions qui indiquent clairement leur objectif est inestimable.

Même les blocs "si" peuvent être bons ou mauvais dans l'auto-documentation. C'est mauvais car vous ne pouvez pas facilement forcer les 2 premières conditions à tester la troisième ... toutes ne sont pas liées:

if (Bill is the boss) && (i == 3) && (the carnival is next weekend)

Celui-ci a plus de sens "collectif" et est plus facile à créer des conditions de test:

if (iRowCount == 2) || (iRowCount == 50) || (iRowCount > 100)

Et cette déclaration n'est qu'une chaîne de caractères aléatoire, vue dans une perspective d'auto-documentation:

var a = F(G1(H1(b1), H2(b2)), G2(c1));

En regardant la déclaration ci-dessus, la maintenabilité est toujours un défi majeur si les fonctions H1 et H2 modifient toutes les deux les mêmes "variables d'état du système" au lieu d'être unifiées en une seule fonction "H", parce que quelqu'un finira par modifier H1 sans même penser qu'il y a un Fonction H2 à regarder et pourrait casser H2.

Je crois qu'une bonne conception de code est très difficile car il n'y a pas de règles strictes qui peuvent être systématiquement détectées et appliquées.

1
Ozymandias

La lisibilité est la partie principale de la maintenabilité. Vous doutez de moi? Choisissez un grand projet dans un langage que vous ne connaissez pas (de préférence le langage de programmation et le langage des programmeurs) et voyez comment vous vous y prendriez pour le refactoriser ...

Je mettrais la lisibilité entre 80 et 90 de maintenabilité. Les 10 à 20 pour cent restants sont à quel point il est possible de refactoriser.

Cela dit, vous passez effectivement 2 variables à votre fonction finale (F). Ces 2 variables sont créées à l'aide de 3 autres variables. Vous auriez mieux fait de passer b1, b2 et c1 dans F, si F existe déjà, puis créez D qui fait la composition pour F et renvoie le résultat. À ce stade, il s'agit simplement de donner un bon nom à D, et peu importe le style que vous utilisez.

Sur un non lié, vous dites que plus de logique sur la page facilite la lisibilité. C'est incorrect, la métrique n'est pas la page, c'est la méthode, et moins la logique qu'une méthode contient, plus elle est lisible.

Lisible signifie que le programmeur peut tenir la logique (entrée, sortie et algorithme) dans sa tête. Plus il en fait, MOINS un programmeur peut le comprendre. Renseignez-vous sur la complexité cyclomatique.

1
jmoreno

Peu importe si vous êtes en C # ou C++, tant que vous êtes dans une version de débogage, une solution possible consiste à encapsuler les fonctions

var a = F(G1(H1(b1), H2(b2)), G2(c1));

Vous pouvez écrire une expression en ligne et toujours indiquer où se situe le problème en regardant simplement la trace de la pile.

returnType F( params)
{
    returnType RealF( params);
}

Bien sûr, si vous appelez la même fonction plusieurs fois sur la même ligne, vous ne pouvez pas savoir quelle fonction, mais vous pouvez toujours l'identifier:

  • Examen des paramètres de fonction
  • Si les paramètres sont identiques et que la fonction n'a pas d'effets secondaires, alors deux appels identiques deviennent 2 appels identiques, etc.

Ce n'est pas une solution miracle, mais pas si mal à mi-chemin.

Sans oublier que l'encapsulation d'un groupe de fonctions peut même être plus bénéfique pour la lisibilité du code:

type CallingGBecauseFTheorem( T b1, C b2)
{
     return G1( H1( b1), H2( b2));
}

var a = F( CallingGBecauseFTheorem( b1,b2), G2( c1));
1
CoffeDeveloper