web-dev-qa-db-fra.com

Comment puis-je simplifier cet ensemble d'instructions if? (Ou qu'est-ce qui le rend si gênant?)

Mon collègue m'a montré ce morceau de code, et nous nous demandions tous les deux pourquoi nous n'arrivions pas à supprimer le code en double.

private List<Foo> parseResponse(Response<ByteString> response) {
    if (response.status().code() != Status.OK.code() || !response.payload().isPresent()) {
      if (response.status().code() != Status.NOT_FOUND.code() || !response.payload().isPresent()) {
        LOG.error("Cannot fetch recently played, got status code {}", response.status());
      }
      return Lists.newArrayList();
    }
    // ...
    // ...
    // ...
    doSomeLogic();
    // ...
    // ...
    // ...
    return someOtherList;
}

Voici une représentation alternative, pour la rendre un peu moins verbeuse:

private void f() {
    if (S != 200 || !P) {
        if (S != 404 || !P) {
            Log();
        }
        return;
    }
    // ...
    // ...
    // ...
    doSomeLogic();
    // ...
    // ...
    // ...
    return;
}

Existe-t-il un moyen plus simple d'écrire ceci, sans dupliquer le !P? Sinon, y a-t-il une propriété unique concernant la situation ou les conditions qui rend impossible de prendre en compte le !P?

31
Andrew Cheong

L'une des raisons pour lesquelles il ressemble à beaucoup de code est qu'il est très répétitif. Utilisez des variables pour stocker les parties qui sont répétées, et cela aidera à la lisibilité:

private List<Foo> parseResponse(Response<ByteString> response) {
    Status status = response.status();
    int code = status.code();
    boolean payloadAbsent = !response.payload().isPresent();

    if (code != Status.OK.code() || payloadAbsent) {
      if (code != Status.NOT_FOUND.code() || payloadAbsent) {
        LOG.error("Cannot fetch recently played, got status code {}", status);
      }
      return Lists.newArrayList();
    }
    // ...
    // ...
    // ...
    return someOtherList;
}

Edit: Comme Stewart le souligne dans les commentaires ci-dessous, s'il est possible de comparer directement response.status() et Status.OK, Vous pouvez éliminer les appels étrangers à .code() et utilisez import static pour accéder directement aux statuts:

import static Namespace.Namespace.Status;

// ...

private List<Foo> parseResponse(Response<ByteString> response) {
    Status status = response.status();
    boolean payloadAbsent = !response.payload().isPresent();

    if (status != OK || payloadAbsent) {
      if (status!= NOT_FOUND || payloadAbsent) {
        LOG.error("Cannot fetch recently played, got status code {}", status);
      }
      return Lists.newArrayList();
    }
    // ...
    // ...
    // ...
    return someOtherList;
}

Concernant la question de savoir quoi faire avec la duplication de la logique payloadAbsent, Zachary a fourni quelques bonnes idées qui sont compatibles avec ce que j'ai suggéré. Une autre option consiste à conserver la structure de base, mais à rendre la raison de la vérification de la charge utile plus explicite. Cela rendrait la logique plus facile à suivre et vous éviterait d'avoir à utiliser || Dans le if intérieur. OTOH, je ne suis pas très attaché à cette approche moi-même:

import static Namespace.Namespace.Status;

// ...

private List<Foo> parseResponse(Response<ByteString> response) {
    Status status = response.status();
    boolean failedRequest = status != OK;
    boolean loggableError = failedRequest && status!= NOT_FOUND ||
        !response.payload().isPresent();

    if (loggableError) {
      LOG.error("Cannot fetch recently played, got status code {}", status);
    }
    if (failedRequest || loggableError) {
      return Lists.newArrayList();
    }
    // ...
    // ...
    // ...
    return someOtherList;
}
29
JLRishe

Si vous souhaitez clarifier les vérifications conditionnelles et conserver le même résultat logique, les éléments suivants peuvent être appropriés.

if (!P) {
    Log();
    return A;
}
if (S != 200) {
    if (S != 404) {
        Log();
    }
    return A;
}
return B;

Ou (c'était OP préféré)

if (S == 404 && P) {
    return A;
}
if (S != 200 || !P) {
    Log();
    return A;
}
return B;

Ou (personnellement, je préfère cela, si cela ne vous dérange pas)

if (P) {
    switch (S) {
        case 200: return B;
        case 404: return A;
    }
}
Log ();
return A;

Vous pourriez condenser l'instruction if en supprimant les accolades et en déplaçant le corps simple sur la même ligne que l'instruction if. Cependant, les instructions if sur une seule ligne peuvent être source de confusion et constituer une mauvaise pratique. D'après ce que je comprends des commentaires, votre préférence serait contre cette utilisation. Alors que les instructions if sur une seule ligne peuvent condenser la logique et donner l'apparence d'un code plus propre, la clarté et l'intention du code doivent être valorisées par rapport au code "économique". Pour être clair: personnellement, je pense qu'il y a des situations où des déclarations if sur une seule ligne sont appropriées, cependant, comme les conditions d'origine sont très longues, je déconseille fortement cela dans ce cas.

if (S != 200 || !P) {
    if (S != 404 || !P) Log();
    return A;
}
return B;

En tant que nœud secondaire: si l'instruction Log(); était la seule branche accessible dans les instructions if imbriquées, vous pouvez utiliser l'identité logique suivante pour condenser la logique ( Distributive ).

(S != 200 || !P) && (S! = 404 || !P) <=> (S != 200 && S != 404) || !P

[~ # ~] modifier [~ # ~] Modification importante pour réorganiser le contenu et résoudre les problèmes mentionnés dans les commentaires.

21
Zachary

N'oubliez pas que la concision n'est pas toujours la meilleure solution et dans la plupart des cas, les variables nommées locales seront optimisées par le compilateur.

J'utiliserais probablement:

    boolean goodPayload = response.status().code() == Status.OK.code() && response.payload().isPresent();
    if (!goodPayload) {
        // Log it if payload not found error or no payload at all.
        boolean logIt = response.status().code() == Status.NOT_FOUND.code()
                || !response.payload().isPresent();
        if (logIt) {
            LOG.error("Cannot fetch recently played, got status code {}", response.status());
        }
        // Use empty.
        return Lists.newArrayList();
    }
13
OldCurmudgeon

Le odeur de code pour moi est que vous êtes demandant de l'objet Response au lieu de disant il . Demandez-vous pourquoi la méthode Parse est externe à l'objet Response au lieu d'en être une méthode (ou, plus probablement, une super-classe). La méthode Log () peut-elle être appelée dans le constructeur d'objet Response au lieu de sa méthode Parse? Au moment où les propriétés status().code() et payload().isPresent() sont calculées dans le constructeur, serait-il possible d'affecter un objet analysé par défaut à une propriété privée telle que seule une simple (et unique) if ... else ... reste dans Parse ()?

Lorsque l'on a la chance d'écrire dans un langage orienté objet avec héritage d'implémentation, chaque instruction conditionnelle (et expression!) Doit être interrogée, pour voir si elle est candidate à être introduite dans le constructeur ou la méthode (s ) qui invoquent le constructeur. La simplification qui peut suivre pour toutes vos conceptions de classe est vraiment immense.

13
Pieter Geerkens

La chose principale qui est réellement redondante est le! P (! La charge utile est présente). Si vous l'avez écrit comme une expression booléenne, vous avez:

(A || !P) && (B || !P)

Comme vous l'avez observé, le! P semble se répéter, et c'est inutile. En algèbre booléenne, vous pouvez traiter ET une sorte de multiplication similaire, et OR une sorte d'addition similaire. Vous pouvez donc étendre ces parenthèses comme avec l'algèbre simple dans:

A && B || A && !P || !P && B || !P && !P

Vous pouvez regrouper toutes les expressions ANDed qui ont! P ensemble:

A && B || (A && !P || !P && B || !P && !P)

Puisque tous ces termes contiennent! P, vous pouvez les supprimer comme une multiplication. Lorsque vous le faites, vous le remplacez par true (comme vous le feriez pour 1, car 1 fois tout est lui-même, true ET tout est lui-même):

A && B || !P && (A && true || B && true || true && true)

Notez que "true && true" est l'une des expressions OR, de sorte que le regroupement entier est toujours vrai, et vous pouvez simplifier pour:

A && B || !P && true
-> A && B || !P

Je suis rouillé sur la notation et la terminologie appropriées ici, peut-être. Mais c'est l'essentiel.

Revenons au code, si vous avez des expressions complexes dans une instruction if, comme d'autres l'ont remarqué, vous devez les coller dans une variable significative même si vous allez simplement l'utiliser une fois et la jeter.

Donc, en les rassemblant, vous obtenez:

boolean statusIsGood = response.status().code() != Status.OK.code() 
  && response.status().code() != Status.NOT_FOUND.code();

if (!statusIsGood || !response.payload().isPresent()) {
  log();
}

Notez que la variable est nommée "statusIsGood" même si vous la niez. Parce que les variables avec des noms négatifs sont vraiment déroutantes.

Gardez à l'esprit, vous pouvez faire le type de simplification ci-dessus pour une logique vraiment complexe, mais vous ne devriez pas toujours le faire. Vous vous retrouverez avec des expressions qui sont techniquement correctes mais personne ne peut dire pourquoi en les regardant.

Dans ce cas, je pense que la simplification clarifie l'intention.

10
Vectorjohn

À mon humble avis, le problème est principalement la répétition et si la nidification. D'autres ont suggéré d'utiliser des variables claires et des fonctions utilitaires que je recommande également, mais vous pouvez également essayer de séparer les préoccupations de vos validations.

Corrigez-moi si je me trompe, mais il semble que votre code essaie de valider avant de réellement traiter la réponse, voici donc une autre façon d'écrire vos validations:

private List<Foo> parseResponse(Response<ByteString> response)
{
    if (!response.payload.isPresent()) {
        LOG.error("Response payload not present");
        return Lists.newArrayList();
    }
    Status status = response.status();
    if (status != Status.OK || status != Status.NOT_FOUND) {
        LOG.error("Cannot fetch recently played, got status code {}", status);
        return Lists.newArrayList();
    }
    // ...
    // ...
    // ...
    return someOtherList;
}
5
emont01

Vous pouvez inverser l'instruction if pour la rendre plus claire comme dans:

private void f() {
    if (S == 200 && P) {
        return;
    }

    if (S != 404 || !P) {
        Log();
    }

    return;
}

Vous pouvez ensuite refactoriser les conditions en utilisant des noms de méthode significatifs tels que "responseIsValid ()" et "responseIsInvalid ()".

4
Kerri Brown

Les fonctions d'aide peuvent simplifier les conditions imbriquées.

private List<Foo> parseResponse(Response<ByteString> response) {
    if (!isGoodResponse(response)) {
        return handleBadResponse(response);
    }
    // ...
    // ...
    // ...
    return someOtherList;
}
3
jxh

La partie la plus gênante est que les getters comme response.status() sont appelés plusieurs fois lorsque la logique semble exiger une seule valeur cohérente. Vraisemblablement, cela fonctionne parce que les getters sont garantis de toujours retourner la même valeur, mais il exprime mal l'intention du code et le rend plus fragile aux hypothèses actuelles.

Pour résoudre ce problème, le code doit obtenir response.status() une fois,

var responseStatus = response.status();

, puis utilisez simplement responseStatus par la suite. Cela doit être répété pour les autres valeurs getter qui sont supposées être la même valeur à chaque obtention.

De plus, tous ces gettings seraient idéalement effectués au même point séquentiel si ce code pouvait plus tard être refactorisé dans une implémentation thread-safe dans un contexte plus dynamique. L'essentiel est que vous voulez obtenir les valeurs de response à un moment donné, de sorte que la section critique du code devrait obtenir ces valeurs en un seul processus synchrone.

En général, la spécification correcte du flux de données prévu rend le code plus résilient et maintenable. Ensuite, si quelqu'un doit ajouter un effet secondaire à un getter ou faire de response un type de données abstrait, il sera beaucoup plus probable de continuer à fonctionner comme prévu.

3
Nat

Je pense que ce qui suit est équivalent. Mais comme d'autres l'ont souligné, la transparence du code peut être plus importante que le code "simple".

if (not ({200,404}.contains(S) && P)){
    log();
    return;
}
if (S !=200){
    return;
}
// other stuff
2
Acccumulation

Avertissement: je ne remettrai pas en question la signature de la fonction présentée, ni la fonctionnalité.

Cela me semble gênant, car la fonction fait beaucoup de travail seule, plutôt que de la déléguer.

Dans ce cas, je suggère de hisser la partie validation:

// Returns empty if valid, or a List if invalid.
private Optional<List<Foo>> validateResponse(Response<ByteString> response) {
    var status = response.status();
    if (status.code() != Status.NOT_FOUND.code() || !response.payload().isPresent()) {
        LOG.error("Cannot fetch recently played, got status code {}", status);
        return Optional.of(Lists.newArrayList());
    }

    if (status.code() != Status.OK.code()) {
        return Optional.of(Lists.newArrayList());
    }

    return Optional.empty();
}

Notez que je préfère répéter l'instruction return plutôt que d'imbriquer des conditions. Cela maintient le code plat, réduisant la complexité cyclomatique. De plus, il n'y a aucune garantie que vous souhaiterez toujours renvoyer le résultat identique pour tous les codes d'erreur.

Ensuite, parseResponse devient facile:

private List<Foo> parseResponse(Response<ByteString> response) {
    var error = validateResponse(response);
    if (error.isPresent()) {
        return error.get();
    }

    // ...
    // ...
    // ...
    return someOtherList;
}

Vous pouvez, à la place, utiliser un style fonctionnel.

/// Returns an instance of ... if valid.
private Optional<...> isValid(Response<ByteString> response) {
    var status = response.status();
    if (status.code() != Status.NOT_FOUND.code() || !response.payload().isPresent()) {
        LOG.error("Cannot fetch recently played, got status code {}", status);
        return Optional.empty();
    }

    if (status.code() != Status.OK.code()) {
        return Optional.empty();
    }

    return Optional.of(...);
}

private List<Foo> parseResponse(Response<ByteString> response) {
    return isValid(response)
        .map((...) -> {
            // ...
            // ...
            // ...
            return someOtherList;
        })
        .orElse(Lists.newArrayList());
}

Bien que personnellement, je trouve que l'emboîtement supplémentaire est un peu ennuyeux.

2
Matthieu M.

Utilisez simplement une variable comme la réponse de JLRishe. Mais je dirais que la clarté du code est beaucoup plus importante que de ne pas dupliquer une simple vérification booléenne. Vous pouvez utiliser des déclarations de retour anticipées pour rendre cela beaucoup plus clair:

private List<Foo> parseResponse(Response<ByteString> response) {

    if (response.status().code() == Status.NOT_FOUND.code() && !response.payload().isPresent()) // valid state, return empty list
        return Lists.newArrayList();

    if (response.status().code() != Status.OK.code()) // status code says something went wrong
    {
        LOG.error("Cannot fetch recently played, got status code {}", response.status());
        return Lists.newArrayList();
    }

    if (!response.payload().isPresent()) // we have an OK status code, but no payload! should this even be possible?
    {
        LOG.error("Cannot fetch recently played, got status code {}, but payload is not present!", response.status());
        return Lists.newArrayList();
    }

    // ... got ok and payload! do stuff!

    return someOtherList;
}
1
user673679

Le branchement sur un entier en le comparant à un ensemble fini de valeurs explicites est mieux géré par un switch:

if (P) {
    switch (S) {
        case 200: return B;
        case 404: return A;
    }
}

Log();
return A;
1
Alexander

vous pouvez avoir un ensemble de codes que vous souhaitez enregistrer et la condition commune de charge utile non présente

Set<Code> codes = {200, 404};
if(!codes.contains(S) && !P){
    log();
}
return new ArrayList<>();

Correction en condition.

1
Java Samurai

Avec cet ensemble de conditions, je ne pense pas qu'il existe un moyen de contourner certains dédoublements. Cependant, je préfère garder mes conditions séparées autant que possible et dupliquer d'autres zones lorsque cela est nécessaire.

Si j'écrivais ceci, conformément au style actuel, ce serait quelque chose comme ceci:

    private void f() {
        if(!P) {
            Log();          // duplicating Log() & return but keeping conditions separate
            return;
        } else if (S != 200) {
            if (S != 404) {
                Log();
            }
            return;
        }

        // ...
        // ...
        // ...
        return;
    }

La simplicité du code comporte certains éléments subjectifs et la lisibilité est extrêmement subjective. Étant donné que, si j'allais écrire cette méthode à partir de zéro, c'est ce que j'aurais donné à mes biais.

private static final String ERR_TAG = "Cannot fetch recently played, got status code {}";

private List<Foo> parseResponse(Response<ByteString> response) {
    List<Foo> list = Lists.newArrayList();

    // similar to @JLRishe keep local variables rather than fetch a duplicate value multiple times
    Status status = response.status();
    int statusCode = status.code();
    boolean hasPayload = response.payload().isPresent();

    if(!hasPayload) {
        // If we have a major error that stomps on the rest of the party no matter
        //      anything else, take care of it 1st.
        LOG.error(ERR_TAG, status);
    } else if (statusCode == Status.OK.code()){
        // Now, let's celebrate our successes early.
        // Especially in this case where success is narrowly defined (1 status code)
        // ...
        // ...
        // ...
        list = someOtherList;
    } else {
        // Now we're just left with the normal, everyday failures.
        // Log them if we can
        if(statusCode != Status.NOT_FOUND.code()) {
            LOG.error(ERR_TAG, status);
        }
    }
    return list;        // One of my biases is trying to keep 1 return statement
                        // It was fairly easy here.
                        // I won't jump through too many hoops to do it though.
}

Si je supprime mes commentaires, cela double presque toujours les lignes de code. Certains diront que cela ne pourrait pas rendre le code plus simple. Pour moi, c'est le cas.

1
Gary99

Il est possible de factoriser le test P répété. Le (pseudo code) suivant est logiquement équivalent au code de votre question.

private List<Foo> f() {
    List<Foo> list(); /*whatever construction*/
    if (P) {
        if (S==200) {
            // ...
            // ...
            // ...
            list.update(/*whatever*/);
        }
        else if (S!=404) {
           Log();
        }
    }
    else {
       Log();
    }
    return list;
}

En termes de lisibilité, j'irais avec ce qui suit (encore une fois le pseudo code):

private bool write_log() {
    return (S!=200 && S!=404) || !P
}
private bool is_good_response() {
    return S==200 && P
}
private List<Foo> f() {
    List<Foo> list(); /*whatever construction*/
    if (write_log()) {
       Log();
    }
    if (is_good_response()) {
        // ...
        // ...
        // ...
        list.update(/*whatever*/);
    }
    return list;
}

avec peut-être des fonctions mieux nommées.

1
Peter Bingham

Je ne suis pas sûr de ce que le code essaie de faire: honnêtement, enregistrer uniquement le statut 404 et renvoyer une liste vide lorsque le code n'est pas 200 donne l'impression que vous essayez d'éviter un NPE ...

Je pense que c'est bien mieux quelque chose comme:

private boolean isResponseValid(Response<ByteString> response){
    if(response == null){
        LOG.error("Invalid reponse!");
        return false;
    }

    if(response.status().code() != Status.OK.code()){
        LOG.error("Invalid status: {}", response.status());
        return false;
    }

    if(!response.payload().isPresent()){
        LOG.error("No payload found for response!");
        return false;
    }
    return true;
}

private List<Foo> parseResponse(Response<ByteString> response) throws InvalidResponseException{
    if(!isResponseValid(response)){
        throw InvalidResponseException("Response is not OK!");
    }

    // logic
}

si pour une raison quelconque, la logique if ne peut pas être modifiée, je déplacerai de toute façon la validation dans une fonction distincte.

Essayez également d'utiliser Java:

LOG.error("")    // should be log.error("")
Status.OK.code() // why couldn't it be a constant like Status.OK, or Status.getOkCode()?
0
Matteo A