web-dev-qa-db-fra.com

Comment faire un appel avec un clearer booléen? Piège booléen

Comme indiqué dans les commentaires de @ benjamin-gruenbaum, cela s'appelle le piège booléen:

Dis que j'ai une fonction comme celle-ci

UpdateRow(var item, bool externalCall);

et dans mon contrôleur, cette valeur pour externalCall sera toujours TRUE. Quelle est la meilleure façon d'appeler cette fonction? J'écris habituellement

UpdateRow(item, true);

Mais je me demande, dois-je déclarer un booléen, juste pour indiquer ce que représente cette "vraie" valeur? Vous pouvez le savoir en regardant la déclaration de la fonction, mais c'est évidemment plus rapide et plus clair si vous venez de voir quelque chose comme

bool externalCall = true;
UpdateRow(item, externalCall);

PD: Je ne sais pas si cette question tient vraiment ici, sinon, où puis-je obtenir plus d'informations à ce sujet?

PD2: Je n'ai marqué aucune langue car je pensais que c'était un problème très générique. Quoi qu'il en soit, je travaille avec c # et la réponse acceptée fonctionne pour c #

76
Mario Garcia

Il n'y a pas toujours une solution parfaite, mais vous avez le choix entre de nombreuses alternatives:

  • Utilisez des arguments nommés , si disponibles dans votre langue. Cela fonctionne très bien et n'a pas d'inconvénients particuliers. Dans certaines langues, tout argument peut être passé en tant qu'argument nommé, par exemple updateRow(item, externalCall: true) ( C # ) ou update_row(item, external_call=True) (Python).

    Votre suggestion d'utiliser une variable distincte est un moyen de simuler des arguments nommés, mais n'a pas les avantages de sécurité associés (il n'y a aucune garantie que vous avez utilisé le nom de variable correct pour cet argument).

  • Utilisez des fonctions distinctes pour votre interface publique, avec de meilleurs noms. C'est une autre façon de simuler des paramètres nommés, en plaçant les valeurs de paramètre dans le nom.

    Ceci est très lisible, mais conduit à beaucoup de passe-partout pour vous, qui écrivez ces fonctions. Il ne peut pas non plus bien gérer l'explosion combinatoire lorsqu'il y a plusieurs arguments booléens. Un inconvénient important est que les clients ne peuvent pas définir cette valeur dynamiquement, mais doivent utiliser if/else pour appeler la fonction correcte.

  • Utilisez une énumération . Le problème avec les booléens est qu'ils sont appelés "vrai" et "faux". Donc, introduisez plutôt un type avec de meilleurs noms (par exemple enum CallType { INTERNAL, EXTERNAL }). Comme avantage supplémentaire, cela augmente la sécurité de type de votre programme (si votre langage implémente des énumérations en tant que types distincts). L'inconvénient des énumérations est qu'elles ajoutent un type à votre API visible publiquement. Pour les fonctions purement internes, cela n'a pas d'importance et les énumérations n'ont pas d'inconvénients importants.

    Dans les langues sans énumérations, des chaînes courtes sont parfois utilisées à la place. Cela fonctionne et peut même être meilleur que les booléens crus, mais il est très sensible aux fautes de frappe. La fonction doit alors immédiatement affirmer que l'argument correspond à un ensemble de valeurs possibles.

Aucune de ces solutions n'a un impact prohibitif sur les performances. Les paramètres et énumérations nommés peuvent être résolus complètement au moment de la compilation (pour un langage compilé). L'utilisation de chaînes peut impliquer une comparaison de chaînes, mais son coût est négligeable pour les petits littéraux de chaînes et la plupart des types d'applications.

152
amon

La bonne solution est de faire ce que vous proposez, mais de l'emballer dans une mini-façade:

void updateRowExternally() {
  bool externalCall = true;
  UpdateRow(item, externalCall);
}

La lisibilité l'emporte sur la micro-optimisation. Vous pouvez vous permettre l'appel de fonction supplémentaire, certainement mieux que vous ne pouvez vous permettre l'effort du développeur d'avoir à rechercher une fois la sémantique du drapeau booléen.

39
Kilian Foth

Disons que j'ai une fonction comme UpdateRow (var item, bool externalCall);

Pourquoi avez-vous une fonction comme celle-ci?

Dans quelles circonstances l'appelleriez-vous avec l'argument externalCall réglé sur des valeurs différentes?

Si l'une provient, disons, d'une application cliente externe et que l'autre se trouve dans le même programme (c'est-à-dire différents modules de code), alors je dirais que vous devriez avoir deux méthodes différentes, une pour chaque cas, peut-être même défini sur des interfaces distinctes.

Si, toutefois, vous effectuiez l'appel en fonction de datum provenant d'une source non-programme (par exemple, un fichier de configuration ou une base de données lue), alors la méthode de passage booléen aurait plus de sens.

25
Phill W.

Bien que je convienne qu'il est idéal d'utiliser une fonctionnalité de langue pour renforcer la lisibilité et la sécurité des valeurs, vous pouvez également opter pour une approche pratique: commentaires au moment de l'appel. Comme:

UpdateRow(item, true /* row is an external call */);

ou:

UpdateRow(item, true); // true means call is external

ou (à juste titre, comme suggéré par Frax):

UpdateRow(item, /* externalCall */true);
17
bishop
  1. Vous pouvez "nommer" vos bools. Voici un exemple pour une langue OO (où elle peut être exprimée dans la classe fournissant UpdateRow()), cependant le concept lui-même peut être appliqué dans n'importe quelle langue:

    class Table
    {
    public:
        static const bool WithExternalCall = true;
        static const bool WithoutExternalCall = false;
    

    et sur le site de l'appel:

    UpdateRow(item, Table::WithExternalCall);
    
  2. Je crois que l'article # 1 est meilleur, mais il n'oblige pas l'utilisateur à utiliser de nouvelles variables lors de l'utilisation de la fonction. Si la sécurité des types est importante pour vous, vous pouvez créer un type enum et faire en sorte que UpdateRow() l'accepte au lieu d'un bool:

    UpdateRow(var item, ExternalCallAvailability ec);

  3. Vous pouvez modifier le nom de la fonction de façon à ce qu'il reflète mieux la signification du paramètre bool. Pas très sûr mais peut-être:

    UpdateRowWithExternalCall(var item, bool externalCall)

11
simurg

Une autre option que je n'ai pas encore lue ici est: utilisez un IDE moderne.

Par exemple IntelliJ IDEA imprime le nom de variable des variables dans la méthode que vous appelez si vous passez un littéral tel que true ou null ou username + “@company.com. Cela se fait dans une petite police afin qu'il n'occupe pas trop d'espace sur votre écran et soit très différent du code réel.

Je ne dis toujours pas que c'est une bonne idée de jeter des booléens partout. L'argument disant que vous lisez du code beaucoup plus souvent que vous n'écrivez est souvent très fort, mais dans ce cas particulier, cela dépend fortement de la technologie que vous (et vos collègues!) Utilisez pour soutenir votre travail. Avec un IDE c'est beaucoup moins un problème qu'avec par exemple vim.

Exemple modifié d'une partie de ma configuration de test: enter image description here

2 jours et personne n'a mentionné le polymorphisme?

target.UpdateRow(item);

Lorsque je suis un client qui souhaite mettre à jour une ligne avec un élément, je ne veux pas penser au nom de la base de données, à l'URL du micro-service, au protocole utilisé pour communiquer, ou si un appel externe est ou non va devoir être utilisé pour y arriver. Arrêtez de me pousser ces détails. Prenez simplement mon article et allez déjà mettre à jour une ligne quelque part.

Faites cela et cela devient une partie du problème de construction. Cela peut être résolu de plusieurs façons. En voici un:

Target xyzTargetFactory(TargetBuilder targetBuilder) {
    return targetBuilder
        .connectionString("some connection string")
        .table("table_name")
        .external()
        .build()
    ; 
}

Si vous regardez cela et pensez "Mais ma langue a nommé des arguments, je n'en ai pas besoin". Très bien, super, allez les utiliser. Gardez simplement ces bêtises hors du client appelant qui ne devrait même pas savoir à quoi il parle.

Il convient de noter que c'est plus qu'un problème sémantique. C'est aussi un problème de conception. Passez un booléen et vous êtes obligé d'utiliser la ramification pour y faire face. Pas très orienté objet. Vous avez deux booléens ou plus à passer? Souhaitant que votre langue soit répartie plusieurs fois? Regardez ce que les collections peuvent faire lorsque vous les imbriquez. La bonne structure de données peut vous simplifier la vie.

6
candied_orange

Si vous pouvez modifier la fonction updateRow, vous pouvez peut-être la refactoriser en deux fonctions. Étant donné que la fonction prend un paramètre booléen, je soupçonne qu'elle ressemble à ceci:

function updateRow(var item, bool externalCall) {
  Database.update(item);

  if (externalCall) {
    Service.call();
  }
}

Cela peut être un peu une odeur de code. La fonction peut avoir un comportement radicalement différent selon la valeur de la variable externalCall, auquel cas elle a deux responsabilités différentes. La refactorisation en deux fonctions qui n'ont qu'une seule responsabilité peut améliorer la lisibilité:

function updateRowAndService(var item) {
  updateRow(item);
  Service.call();
}

function updateRow(var item) {
  Database.update(item);
}

Désormais, partout où vous appelez ces fonctions, vous pouvez voir en un coup d'œil si le service externe est appelé.

Ce n'est pas toujours le cas, bien sûr. C'est une situation et une question de goût. La refactorisation d'une fonction qui prend un paramètre booléen en deux fonctions vaut généralement la peine d'être considérée, mais n'est pas toujours le meilleur choix.

2
Kevin

Si UpdateRow est dans une base de code contrôlée, je considérerais le modèle de stratégie:

public delegate void Request(string query);    
public void UpdateRow(Item item, Request request);

Où Request représente une sorte de DAO (un rappel en cas trivial).

Vrai cas:

UpdateRow(item, query =>  queryDatabase(query) ); // perform remote call

Faux cas:

UpdateRow(item, query => readLocalCache(query) ); // skip remote call

Habituellement, l'implémentation du point de terminaison serait configurée à un niveau d'abstraction plus élevé et je l'utiliserais simplement ici. Comme effet secondaire gratuit utile, cela me donne la possibilité de mettre en œuvre une autre façon d'accéder aux données distantes, sans aucune modification du code:

UpdateRow(item, query => {
  var data = readLocalCache(query);
  if (data == null) {
    data = queryDatabase(query);
  }
  return data;
} );

En général, une telle inversion de contrôle garantit un couplage plus faible entre votre stockage de données et votre modèle.

0
Basilevs