Est-ce un bon schéma: remplacer une fonction longue par une série de lambdas?

14

J'ai récemment rencontré la situation suivante.

class A{
public:
    void calculate(T inputs);
}

Premièrement, Areprésente un objet dans le monde physique, ce qui est un argument fort pour ne pas diviser la classe. Maintenant, cela calculate()s'avère être une fonction assez longue et compliquée. J'en perçois trois structures possibles:

  • l'écrire comme un mur de texte - avantages - toutes les informations sont en un seul endroit
  • écrire des privatefonctions utilitaires dans la classe et les utiliser dans calculatele corps de - inconvénients - le reste de la classe ne sait pas / ne prend pas soin / ne comprend pas ces méthodes
  • écrivez de calculatela manière suivante:

    void A::calculate(T inputs){    
        auto lambda1 = () [] {};    
        auto lambda2 = () [] {};    
        auto lambda3 = () [] {};
    
        lambda1(inputs.first_logical_chunk);
        lambda2(inputs.second_logical_chunk);
        lambda3(inputs.third_logical_chunk);
    }

Cela peut-il être considéré comme une bonne ou une mauvaise pratique? Cette approche révèle-t-elle des problèmes? Dans l'ensemble, dois-je considérer cela comme une bonne approche lorsque je suis à nouveau confronté à la même situation?


ÉDITER:

class A{
    ...
public:
    // Reconfiguration of the algorithm.
    void set_colour(double colour);
    void set_density(double density);
    void set_predelay(unsigned long microseconds);
    void set_reverb_time(double reverb_time, double room_size);
    void set_drywet(double left, double right);
    void set_room_size(double value);;

private:
    // Sub-model objects.
    ...
}

Toutes ces méthodes:

  • obtenir une valeur
  • calculer d'autres valeurs, sans utiliser l'état
  • appeler certains des "objets de sous-modèle" pour changer leur état.

Il s'avère que, sauf que set_room_size()ces méthodes transmettent simplement la valeur demandée aux sous-objets. set_room_size(), d'autre part, fait un couple d'écrans de formules obscures, puis (2) fait un demi-écran d'appel de sous-objets setters pour appliquer les différents résultats obtenus. Par conséquent, j'ai séparé la fonction en deux lambdas et je les appelle à la fin de la fonction. Si j'avais pu le diviser en morceaux plus logiques, j'aurais isolé plus de lambdas.

Quoi qu'il en soit, l'objectif de la question actuelle est de déterminer si cette façon de penser doit persister, ou au mieux ne pas ajouter de valeur (lisibilité, maintenabilité, capacité de débogage, etc.).

Vorac
la source
2
Qu'est-ce que vous pensez que l'utilisation de lambdas vous évitera les appels de fonction?
Blrfl
1
Firstly, A represents an object in the physical world, which is a strong argument for not splitting the class up.AReprésente sûrement des données sur un objet qui pourrait exister dans le monde physique. Vous pouvez avoir une instance de Asans l'objet réel et un objet réel sans une instance de A, donc les traiter comme si elles étaient une seule et même chose n'a pas de sens.
Doval
@Blrfl, encapsulation - personne calculate()ne connaît ces sous-fonctions.
Vorac
Si tous ces calculs concernent juste A, cela va un peu à l'extrême.
Blrfl
1
"Premièrement, Areprésente un objet dans le monde physique, ce qui est un argument solide pour ne pas diviser la classe." On m'a malheureusement dit cela lorsque j'ai commencé à programmer. Il m'a fallu des années pour réaliser que c'est un tas de hockey sur cheval. C'est une raison terrible de grouper des choses. Je ne peux pas expliquer quelles sont les bonnes raisons de grouper les choses (au moins à ma satisfaction), mais celle-là est celle que vous devriez rejeter dès maintenant. En fin de compte, tout le "bon code" est qu'il fonctionne correctement, qu'il est relativement facile à comprendre et qu'il est relativement facile de changer (c'est-à-dire que les changements n'ont pas d'effets secondaires étranges).
jpmc26

Réponses:

13

Non, ce n'est généralement pas un bon schéma

Ce que vous faites est de diviser une fonction en fonctions plus petites à l'aide de lambda. Cependant, il existe un bien meilleur outil pour décomposer les fonctions: les fonctions.

Les lambdas fonctionnent, comme vous l'avez vu, mais ils signifient beaucoup beaucoup beaucoup plus que la simple décomposition d'une fonction en bits locaux. Les Lambdas font:

  • Fermetures. Vous pouvez utiliser des variables dans la portée externe à l'intérieur du lambda. C'est très puissant et très compliqué.
  • Réaffectation. Bien que votre exemple rende difficile la tâche d'affectation, il faut toujours faire attention à l'idée que le code peut permuter les fonctions à tout moment.
  • Fonctions de première classe. Vous pouvez passer une fonction lambda à une autre fonction, en faisant quelque chose appelé «programmation fonctionnelle».

À l'instant où vous introduisez des lambdas dans le mélange, le prochain développeur à regarder le code doit immédiatement charger mentalement toutes ces règles afin de voir comment fonctionne votre code. Ils ne savent pas que vous n'utiliserez pas toutes ces fonctionnalités. C'est très cher par rapport aux alternatives.

C'est comme utiliser une pelle rétrocaveuse pour faire votre jardinage. Vous savez que vous ne l'utilisez que pour creuser de petits trous pour les fleurs de cette année, mais les voisins deviendront nerveux.

Considérez que tout ce que vous faites est de regrouper visuellement votre code source. Le compilateur ne se soucie pas vraiment que vous curiez les choses avec des lambdas. En fait, je m'attendrais à ce que l'optimiseur annule immédiatement tout ce que vous venez de faire lors de la compilation. Vous vous adressez uniquement au lecteur suivant (merci de le faire, même si nous ne sommes pas d'accord sur la méthodologie! Le code est lu beaucoup plus souvent qu'il n'est écrit!). Tout ce que vous faites, c'est regrouper des fonctionnalités.

  • Les fonctions placées localement dans le flux de code source feraient tout aussi bien, sans invoquer lambda. Encore une fois, tout ce qui compte, c'est que le lecteur puisse le lire.
  • Commentaires en haut de la fonction indiquant "nous divisons cette fonction en trois parties", suivis de grandes longues lignes // ------------------entre chaque partie.
  • Vous pouvez également placer chaque partie du calcul dans sa propre étendue. Cela a l'avantage de prouver immédiatement hors de tout doute qu'il n'y a pas de partage variable entre les pièces.

EDIT: En voyant votre édition avec un exemple de code, je penche pour que la notation des commentaires soit la plus propre, avec des crochets pour appliquer les limites suggérées par les commentaires. Cependant, si l'une des fonctionnalités était réutilisable dans d'autres fonctions, je recommanderais d'utiliser des fonctions à la place

void A::set_room_size(double value)
{
    {
        // Part 1: {description of part 1}
        ...
    }
    // ------------------------
    {
        // Part 2: {description of part 2}
        ...
    }
    // ------------------------
    {
        // Part 3: {description of part 3}
        ...
    }
}
Cort Ammon
la source
Ce n'est donc pas un nombre objectif, mais je dirais subjectivement que la simple présence de fonctions lambda me fait prêter environ 10 fois plus d'attention à chaque ligne de code, car elles ont tellement de potentiel pour devenir dangereusement compliquées, rapides et ainsi de suite une ligne de code à l'air innocent (comme count++)
Cort Ammon
Bon point. Je vois cela comme les quelques avantages de l'approche avec lambdas - (1) code localement adjacent et avec portée locale (cela serait perdu par les fonctions au niveau du fichier) (2) le compilateur garantit qu'aucune variable locale n'est partagée entre les segments de code. Ces avantages peuvent donc être préservés en les séparant calculate()en {}blocs et en déclarant des données partagées au niveau de la calculate()portée. Je pensais qu'en voyant que les lambdas ne capturaient pas, un lecteur ne serait pas encombré par le pouvoir des lambdas.
Vorac du
"Je pensais qu'en voyant les lambdas ne pas capturer, un lecteur ne serait pas encombré par le pouvoir des lambdas." C'est en fait une déclaration juste, mais controversée, qui touche au cœur de la linguistique. Les mots ont généralement des connotations qui dépassent leurs dénotations. Que ma connotation lambdasoit injuste ou que vous obligiez grossièrement les gens à suivre la dénotation stricte n'est pas une question facile à répondre. En fait, il peut être tout à fait acceptable pour vous d'utiliser de lambdacette façon dans votre entreprise, et totalement inacceptable dans ma société, et ni l'un ni l'autre ne doit se tromper!
Cort Ammon
Mon opinion sur la connotation de lambda vient du fait que j'ai grandi en C ++ 03, pas en C + 11. J'ai passé des années à développer une appréciation pour les endroits spécifiques où C ++ a été blessé par un manque de lambda, comme la for_eachfonction. Par conséquent, lorsque je vois un lambdaqui ne correspond pas à l'un de ces cas de panne faciles à repérer, la première hypothèse à laquelle j'arrive est qu'il sera probablement utilisé pour la programmation fonctionnelle, car il n'était pas nécessaire autrement. Pour de nombreux développeurs, la programmation fonctionnelle est un état d'esprit complètement différent de la programmation procédurale ou OO.
Cort Ammon
Merci d'avoir expliqué. Je suis un programmeur novice et je suis maintenant heureux d'avoir posé la question - avant que l'habitude ne se soit développée.
Vorac
20

Je pense que vous avez fait une mauvaise hypothèse:

Premièrement, A représente un objet dans le monde physique, ce qui est un argument solide pour ne pas diviser la classe.

Je suis en désaccord avec cela. Par exemple, si j'avais une classe qui représente une voiture, je voudrais certainement la diviser, car je veux sûrement une classe plus petite pour représenter les pneus.

Vous devez diviser cette fonction en petites fonctions privées. Si elle semble vraiment séparée de l'autre partie de la classe, cela peut être un signe que la classe doit être séparée. Bien sûr, c'est difficile à dire sans un exemple explicite.

Je ne vois pas vraiment l'avantage d'utiliser les fonctions lambda dans ce cas, car cela ne rend pas vraiment le code plus propre. Ils ont été créés pour faciliter la programmation de style fonctionnel, mais ce n'est pas ça.

Ce que vous avez écrit ressemble un peu aux objets de fonction imbriqués de style Javascript. Ce qui est encore une fois le signe qu'ils sont étroitement liés. Etes-vous sûr que vous ne devriez pas faire une classe séparée pour eux?

Pour résumer, je ne pense pas que ce soit un bon schéma.

MISE À JOUR

Si vous ne voyez aucun moyen d'encapsuler cette fonctionnalité dans une classe significative, vous pouvez créer des fonctions d'assistance à portée de fichier, qui ne sont pas membres de votre classe. C'est du C ++ après tout, la conception OO n'est pas un must.

Gábor Angyal
la source
Cela semble raisonnable, mais il m'est difficile d'imaginer la mise en œuvre. Classe de portée de fichier avec des méthodes statiques? Classe, définie à l'intérieur A(peut-être même foncteur avec toutes les autres méthodes privées)? Classe déclarée et définie à l'intérieur calculate()(cela ressemble beaucoup à mon exemple lambda). À titre de clarification, calculate()fait partie d'une famille de méthodes ( calculate_1(), calculate_2()etc.) dont toutes sont simples, celle-ci est juste 2 écrans de formules.
Vorac
@Vorac: Pourquoi est-ce calculate()tellement plus long que toutes les autres méthodes?
Kevin
@Vorac Il est vraiment difficile d'aider sans voir votre code. Pouvez-vous également l'afficher?
Gábor Angyal, du
@Kevin, les formules pour tout sont données dans les conditions. Code affiché.
Vorac
4
Je voterais 100 fois si je le pouvais. "Modéliser des objets dans le monde réel" est le début de la spirale de la mort de la conception d'objets. C'est un drapeau rouge géant dans n'importe quel code.
Fred the Magic Wonder Dog