web-dev-qa-db-fra.com

Une grande expression booléenne est-elle plus lisible que la même expression décomposée en méthodes de prédicat?

Qu'est-ce qui est plus facile à comprendre, une grosse instruction booléenne (assez complexe), ou la même instruction décomposée en méthodes de prédicat (beaucoup de code supplémentaire à lire)?

Option 1, la grande expression booléenne:

    private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
    {

        return propVal.PropertyId == context.Definition.Id
            && !repo.ParentId.HasValue || repo.ParentId == propVal.ParentId
            && ((propVal.SecondaryFilter.HasValue && context.SecondaryFilter.HasValue && propVal.SecondaryFilter.Value == context.SecondaryFilter) || (!context.SecondaryFilter.HasValue && !propVal.SecondaryFilter.HasValue));
    }

Option 2, Les conditions décomposées en méthodes de prédicat:

    private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
    {
        return MatchesDefinitionId(context, propVal)
            && MatchesParentId(propVal)
            && (MatchedSecondaryFilter(context, propVal) || HasNoSecondaryFilter(context, propVal));
    }

    private static bool HasNoSecondaryFilter(CurrentSearchContext context, TValToMatch propVal)
    {
        return (!context.No.HasValue && !propVal.SecondaryFilter.HasValue);
    }

    private static bool MatchedSecondaryFilter(CurrentSearchContext context, TValToMatch propVal)
    {
        return (propVal.SecondaryFilter.HasValue && context.No.HasValue && propVal.SecondaryFilter.Value == context.No);
    }

    private bool MatchesParentId(TValToMatch propVal)
    {
        return (!repo.ParentId.HasValue || repo.ParentId == propVal.ParentId);
    }

    private static bool MatchesDefinitionId(CurrentSearchContext context, TValToMatch propVal)
    {
        return propVal.PropertyId == context.Definition.Id;
    }

Je préfère la deuxième approche, car je vois les noms de méthode comme des commentaires, mais je comprends que c'est problématique parce que vous devez lire toutes les méthodes pour comprendre ce que fait le code, donc il résume l'intention du code.

63
willem

Qu'est-ce qui est plus facile à comprendre

Cette dernière approche. C'est non seulement plus facile à comprendre, mais aussi plus facile à écrire, à tester, à refactoriser et à étendre. Chaque condition requise peut être découplée et traitée en toute sécurité à sa manière.

c'est problématique car il faut lire toutes les méthodes pour comprendre le code

Ce n'est pas problématique si les méthodes sont nommées correctement. En fait, il serait plus facile à comprendre car le nom de la méthode décrirait l'intention de la condition.
Pour un spectateur if MatchesDefinitionId() est plus explicatif que if (propVal.PropertyId == context.Definition.Id)

[Personnellement, la première approche me fait mal aux yeux.]

88
wonderbell

Si c'est le seul endroit où ces fonctions de prédicat seraient utilisées, vous pouvez également utiliser des variables locales bool à la place:

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
{
    bool matchesDefinitionId = (propVal.PropertyId == context.Definition.Id);
    bool matchesParentId = (!repo.ParentId.HasValue || repo.ParentId == propVal.ParentId);
    bool matchesSecondaryFilter = (propVal.SecondaryFilter.HasValue && context.No.HasValue && propVal.SecondaryFilter.Value == context.No);
    bool hasNoSecondaryFilter = (!context.No.HasValue && !propVal.SecondaryFilter.HasValue);

    return matchesDefinitionId
        && matchesParentId
        && matchesSecondaryFilter || hasNoSecondaryFilter;
}

Ceux-ci pourraient également être décomposés et réorganisés pour les rendre plus lisibles, par exemple avec

bool hasSecondaryFilter = propVal.SecondaryFilter.HasValue;

puis en remplaçant toutes les instances de propVal.SecondaryFilter.HasValue. Une chose qui ressort immédiatement est que hasNoSecondaryFilter utilise un ET logique sur les propriétés HasValue négatives, tandis que matchesSecondaryFilter utilise un ET logique sur HasValue non-négativement - donc ce n'est pas exactement le contraire.

44
Simon Richter

En général, ce dernier est préféré.

Cela rend le site d'appel plus réutilisable. Il prend en charge DRY (ce qui signifie que vous avez moins d'endroits à modifier lorsque les critères changent et que vous pouvez le faire de manière plus fiable). Et très souvent, ces sous-critères sont des choses qui seront réutilisées indépendamment ailleurs, permettant vous de le faire.

Oh, et cela facilite grandement les tests unitaires, vous donnant l'assurance que vous l'avez fait correctement.

41
Telastyn

Si c'est entre ces deux choix, alors ce dernier est meilleur. Ce ne sont cependant pas les seuls choix! Que diriez-vous de diviser la fonction unique en plusieurs ifs? Testez les moyens de quitter la fonction pour éviter des tests supplémentaires, en émulant grossièrement un "court-circuit" dans un test sur une seule ligne.

C'est plus facile à lire (vous devrez peut-être vérifier la logique de votre exemple, mais le concept est vrai):

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
{
    if( propVal.PropertyId != context.Definition.Id ) return false;

    if( repo.ParentId.HasValue || repo.ParentId != propVal.ParentId ) return false;

    if( propVal.SecondaryFilter.HasValue && 
        context.SecondaryFilter.HasValue && 
        propVal.SecondaryFilter.Value == context.SecondaryFilter ) return true;

    if( !context.SecondaryFilter.HasValue && 
        !propVal.SecondaryFilter.HasValue) return true;

    return false;   
}
23
BuvinJ

J'aime mieux l'option 2, mais suggérerais un changement structurel. Combinez les deux contrôles sur la dernière ligne du conditionnel en un seul appel.

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
{
    return MatchesDefinitionId(context, propVal)
        && MatchesParentId(propVal)
        && MatchesSecondaryFilterIfPresent(context, propVal);
}

private static bool MatchesSecondaryFilterIfPresent(CurrentSearchContext context, 
                                                    TValToMatch propVal)
{
    return MatchedSecondaryFilter(context, propVal) 
               || HasNoSecondaryFilter(context, propVal);
}

La raison pour laquelle je suggère cela est que les deux vérifications sont une seule unité fonctionnelle, et l'imbrication de parenthèses dans un conditionnel est sujette aux erreurs: à la fois du point de vue de l'écriture initiale du code et du point de vue de la personne qui le lit. C'est particulièrement le cas si les sous-éléments de l'expression ne suivent pas le même schéma.

Je ne sais pas si MatchesSecondaryFilterIfPresent() est le meilleur nom pour la combinaison; mais rien de mieux ne vient immédiatement à l'esprit.

Bien qu'en C #, le code n'est pas très orienté objet. Il utilise des méthodes statiques et ce qui ressemble à des champs statiques (par exemple repo). Il est généralement admis que la statique rend votre code difficile à refactoriser et difficile à tester, tout en gênant la réutilisabilité, et, à votre question: une utilisation statique comme celle-ci est moins lisible et maintenable que la construction orientée objet.

Vous devez convertir ce code dans un formulaire plus orienté objet. Lorsque vous le faites, vous constaterez qu'il existe des endroits judicieux pour mettre du code qui compare les objets, les champs, etc. Il est probable que vous puissiez alors demander aux objets de se comparer, ce qui réduirait votre grande instruction if à simple demande de comparaison (par exemple if ( a.compareTo (b) ) { }, qui pourrait inclure toutes les comparaisons de champs.)

C # dispose d'un riche ensemble d'interfaces et d'utilitaires système pour effectuer des comparaisons sur les objets et leurs champs. Au-delà de l'évidence .Equals, pour commencer, examinez IEqualityComparer, IEquatable et les utilitaires comme System.Collections.Generic.EqualityComparer.Default.

2
Erik Eidt

Le premier est absolument horrible. Vous utilisez || pour deux choses sur la même ligne; c'est soit un bogue dans votre code, soit une intention d'obscurcir votre code.

    return (   (   propVal.PropertyId == context.Definition.Id
                && !repo.ParentId.HasValue)
            || (   repo.ParentId == propVal.ParentId
                && (   (   propVal.SecondaryFilter.HasValue
                        && context.SecondaryFilter.HasValue 
                        && propVal.SecondaryFilter.Value == context.SecondaryFilter)
                    || (   !context.SecondaryFilter.HasValue
                        && !propVal.SecondaryFilter.HasValue))));

C'est au moins à mi-chemin décemment formaté (si le formatage est compliqué, c'est parce que la condition if est compliquée), et vous avez au moins une chance de comprendre si quelque chose là-dedans est absurde. Par rapport à vos ordures formatées si, autre chose est mieux. Mais vous semblez être capable de ne faire que des extrêmes: soit un désordre complet d'une instruction if, soit quatre méthodes complètement inutiles.

Notez que (cond1 && cond2) || (! cond1 && cond3) peut s'écrire

cond1 ? cond2 : cond3

ce qui réduirait le gâchis. J'écrirais

if (propVal.PropertyId == context.Definition.Id && !repo.ParentId.HasValue) {
    return true;
} else if (repo.ParentId != propVal.ParentId) {
    return false;
} else if (propVal.SecondaryFilter.HasValue) {
    return (   context.SecondaryFilter.HasValue
            && propVal.SecondaryFilter.Value == context.SecondaryFilter); 
} else {
    return !context.SecondaryFilter.HasValue;
}
0
gnasher729

Ce dernier est définitivement préféré, j'ai vu des cas avec la première façon et c'est presque toujours impossible à lire. J'ai fait l'erreur de le faire de la première manière et on m'a demandé de le changer pour des méthodes de prédicat.

0
Snoop

Je dirais que les deux sont à peu près les mêmes, SI vous ajoutez des espaces pour la lisibilité et quelques commentaires pour aider le lecteur sur les parties les plus obscures.

Rappelez-vous: un bon commentaire dit au lecteur ce que vous pensiez lorsque vous avez écrit le code.

Avec des changements comme je l'ai suggéré, j'irais probablement avec l'ancienne approche, car elle est moins encombrée et diffuse. Les appels de sous-programme sont comme des notes de bas de page: ils fournissent des informations utiles mais perturbent le flux de lecture. Si les prédicats étaient plus complexes, je les décomposerais en méthodes distinctes afin que les concepts qu'ils incarnent puissent être construits en morceaux compréhensibles.

0
Mark Wood

Eh bien, s'il y a des pièces que vous voudrez peut-être réutiliser, les séparer en fonctions distinctes correctement nommées est évidemment la meilleure idée.
Même si vous ne pouvez jamais les réutiliser, cela pourrait vous permettre de mieux structurer vos conditions et de leur donner une étiquette décrivant ce qu'elles moyenne.

Maintenant, regardons votre première option, et admettons que ni votre indentation ni votre saut de ligne n'étaient utiles, ni que le conditionnel n'était si bien structuré:

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal) {
    return propVal.PropertyId == context.Definition.Id && !repo.ParentId.HasValue
        || repo.ParentId == propVal.ParentId
        && propVal.SecondaryFilter.HasValue == context.SecondaryFilter.HasValue
        && (!propVal.SecondaryFilter.HasValue || propVal.SecondaryFilter.Value == context.SecondaryFilter.Value);
}
0
Deduplicator