web-dev-qa-db-fra.com

Manière élégante de passer plusieurs arguments à une fonction

J'ai une fonction qui ressemble à ceci:

bool generate_script (bool net, bool tv, bool phone,
                        std::string clientsID,
                        std::string password,
                        int index, std::string number, 
                        std::string Iport, std::string sernoID,
                        std::string VoiP_number, std::string  VoiP_pass,
                        std::string target, int slot, int port, 
                        int onu, int extra, std::string IP, std::string MAC);

À mon avis, ça a l'air moche. Quelle est la bonne façon de gérer ce problème? Dois-je créer quelques vecteurs avec différents types de données (int, chaîne et booléen) et les passer comme arguments à cette fonction?

41
Tomasz Kasperczyk

Si tous ces paramètres sont significativement liés, regroupez-les dans une structure.

77
Quentin

Mettez-les dans un struct

Créer une structure

struct GenerateScriptParams { /* ... */ };

et mettre tous les paramètres là-dedans. Vous pouvez également fournir des valeurs par défaut pour l'initialisation de struct en implémentant un constructeur par défaut ou, en C++ 11, en fournissant une initialisation par défaut de membres individuels. Vous pouvez ensuite modifier les valeurs qui ne sont pas censées être par défaut. Cette sélection sélective de paramètres non définis par défaut n'est pas possible pour un appel de fonction avec beaucoup de paramètres en C++.

Rendre l'interface agréable pour l'appelant

Pourtant, l'utilisation est un peu moche, car vous devez créer un objet de nom temporaire, puis modifier les valeurs qui ne devraient pas être par défaut, puis passer l'objet à la fonction:

GenerateScriptParams gsp;
gsp.net = true;
gsp.phone = false;
gps.extra = 10;
generate_script( gsp );

Si vous appelez cette fonction à plusieurs endroits différents, il est logique d'éviter cette laideur en fournissant des fonctions membres mutantes qui peuvent être chaînées:

GenerateScriptParams & GenerateScriptParams::setNet  ( bool val );
GenerateScriptParams & GenerateScriptParams::setTV   ( bool val );
GenerateScriptParams & GenerateScriptParams::setPhone( bool val );
// ... //

Le code appelant peut alors écrire

generate_script( GenerateScriptParams()
    .setNet(true),
    .setPhone(false),
    .setExtra(10) );

sans la laideur ci-dessus. Cela évite l'objet nommé qui n'est utilisé qu'une seule fois.

44
Ralph Tandetzky

Personnellement, je ne pense pas que déplacer tous les arguments dans une seule structure rendra le code bien meilleur. Vous déplacez simplement la saleté sous le tapis. Lorsque vous allez gérer la création de la structure, vous avez le même problème.

La question est de savoir combien cette structure sera réutilisable? Si vous vous retrouvez avec 18 paramètres pour une fonction, appelez quelque chose, cela ne correspond pas tout à fait à votre conception. Après une analyse plus approfondie, vous pouvez découvrir que ces paramètres peuvent être regroupés en plusieurs classes différentes et ces classes peuvent être agrégées en un seul objet qui sera l'entrée de votre fonction. Vous pouvez également préférer les classes à structurer afin de protéger vos données.

MODIFIER

Je vais vous donner un petit exemple pour décrire pourquoi plusieurs classes valent mieux qu'une structure monolithique. Commençons par compter les tests que vous devez écrire pour couvrir la fonction ci-dessus. Il y a 18 paramètres en entrée (3 booléens). Nous allons donc avoir besoin d'au moins 15 tests uniquement pour valider l'entrée (en supposant que les valeurs ne sont pas interconnectées).

Le nombre total de tests est impossible à calculer sans la mise en œuvre, mais nous pouvons avoir une idée de l'ampleur. Soit la borne inférieure, toutes les entrées peuvent être traitées comme booléennes, le nombre de combinaisons possibles est 2 ^ 18, donc environ 262000 tests.

Maintenant, que se passe-t-il si nous divisons l'entrée en plusieurs objets?

Tout d'abord, le code pour valider l'entrée est déplacé de la fonction vers le corps de chaque objet (et peut être réutilisé).

Mais plus important encore, le nombre de tests s'effondrera, disons par groupe de quatre (4,4,4 et 4 paramètres par objet), le nombre total de tests n'est que de:

2 ^ 4 + 2 ^ 4 + 2 ^ 4 + 2 ^ 4 + 2 ^ 4 = 80

Le cinquième attribut est dû à la permutation des objets avec eux-mêmes.

Alors, qu'est-ce qui est plus coûteux? Ecrire des milliers de tests ou quelques classes supplémentaires?

De toute évidence, il s'agit d'une simplification grossière, mais elle sous-tendra le cœur du problème. ne interface encombrée n'est pas seulement une question de style ou un inconvénient pour le développeur, c'est un véritable obstacle à la production de code de qualité.

C'est la leçon la plus importante que j'aie jamais apprise dans ma carrière de développeur professionnel: "Les grosses classes et les grosses interfaces sont mauvaises". C'est juste ma version heuristique du principe de responsabilité unique (j'ai remarqué que le SRP peut être difficile à bien faire, ce qui semble raisonnable d'être à responsabilité unique, il peut ne pas être tout à fait le même après une heure de codage, j'ai donc utilisé une heuristique règle pour m'aider à revalider mes choix initiaux).

20
Alessandro Teruzzi

Ou vous pouvez utiliser une interface fluide . Cela ressemblerait à ceci:

script my_script(mandatory, parameters);
my_script.net(true).tv(false).phone(true);

Cela s'applique si vous avez des valeurs par défaut pour vos paramètres spécifiés ou s'il est autorisé d'avoir un script partiellement construit.

13
PovilasB

Ignorer la possibilité ou l'opportunité de modifier la fonction ou le programme de manière à réduire le nombre de paramètres ...

J'ai vu des normes de codage qui spécifient la durée de formatage des listes de paramètres, dans les cas où une refactorisation n'est pas possible. Un tel exemple utilise des indentations doubles et un paramètre par ligne (pas pour toutes les fonctions - uniquement pour celles qui ont plusieurs lignes de paramètres).

Par exemple.

bool generate_script (
        bool net,
        bool tv,
        bool phone,
        std::string clientsID,
        std::string password,
        int index,
        std::string number,
        std::string Iport,
        std::string sernoID,
        std::string VoiP_number,
        std::string  VoiP_pass,
        std::string target,
        int slot,
        int port,
        int onu,
        int extra,
        std::string IP,
        std::string MAC);

Le but ici est de créer une disposition cohérente et de rechercher toutes les fonctions avec un grand nombre de paramètres.

8
izb

Un peu tard ici, mais comme personne ne l'a encore fait, je voudrais souligner un aspect évident du problème: pour moi, une fonction qui prend autant d'arguments est susceptible de faire beaucoup de calculs, alors envisagez la possibilité de le décomposer en petites fonctions dans un premier temps.

Cela devrait vous aider à structurer vos données.

1
bassfault