alternatives aux prises d'essai imbriquées pour les replis

14

J'ai une situation où j'essaie de récupérer un objet. Si la recherche échoue, plusieurs solutions de rechange sont en place, chacune pouvant échouer. Le code ressemble donc à:

try {
    return repository.getElement(x);
} catch (NotFoundException e) {
    try {
        return repository.getSimilarElement(x);
    } catch (NotFoundException e1) {
        try {
            return repository.getParentElement(x);
        } catch (NotFoundException e2) {
            //can't recover
            throw new IllegalArgumentException(e);
        }
    }
}

Cela a l'air horriblement laid. Je déteste retourner null, mais est-ce mieux dans cette situation?

Element e = return repository.getElement(x);
if (e == null) {
    e = repository.getSimilarElement(x);
}
if (e == null) {
    e = repository.getParentElement(x);
}
if (e == null) {
    throw new IllegalArgumentException();
}
return e;

Existe-t-il d'autres alternatives?

L'utilisation de blocs try-catch imbriqués est-elle un anti-modèle? est liée, mais les réponses sont du type "parfois, mais c'est généralement évitable", sans dire quand ni comment l'éviter.

Alex Wittig
la source
1
Est-ce NotFoundExceptionquelque chose d'exceptionnel?
Je ne sais pas, et c'est probablement pourquoi j'ai des problèmes. C'est dans un contexte de commerce électronique, où les produits sont interrompus quotidiennement. Si quelqu'un met en signet un produit qui est interrompu par la suite, puis essaie d'ouvrir le signet ... est-ce exceptionnel?
Alex Wittig
@FiveNine à mon avis, certainement non - il faut s'y attendre. Voir stackoverflow.com/questions/729379/…
Konrad Morawski

Réponses:

17

La manière habituelle d'éliminer l'imbrication est d'utiliser des fonctions:

Element getElement(x) {
    try {
        return repository.getElement(x);
    } catch (NotFoundException e) {
        return fallbackToSimilar(x);
    }  
}

Element fallbackToSimilar(x) {
    try {
        return repository.getSimilarElement(x);
     } catch (NotFoundException e1) {
        return fallbackToParent(x);
     }
}

Element fallbackToParent(x) {
    try {
        return repository.getParentElement(x);
    } catch (NotFoundException e2) {
        throw new IllegalArgumentException(e);
    }
}

Si ces règles de secours sont universelles, vous pouvez envisager de l'implémenter directement dans l' repositoryobjet, où vous pourrez peut-être simplement utiliser des ifinstructions simples au lieu d'une exception.

Karl Bielefeldt
la source
1
Dans ce contexte, methodserait un meilleur mot que function.
Sulthan
12

Ce serait vraiment facile avec quelque chose comme une monade Option. Malheureusement, Java n'en a pas. Dans Scala, j'utiliserais le Trytype pour trouver la première solution réussie.

Dans mon état d'esprit de programmation fonctionnelle, je mettais en place une liste de rappels représentant les différentes sources possibles, et les parcourais jusqu'à ce que nous trouvions la première réussie:

interface ElementSource {
    public Element get();
}

...

final repository = ...;

// this could be simplified a lot using Java 8's lambdas
List<ElementSource> sources = Arrays.asList(
    new ElementSource() {
        @Override
        public Element get() { return repository.getElement(); }
    },
    new ElementSource() {
        @Override
        public Element get() { return repository.getSimilarElement(); }
    },
    new ElementSource() {
        @Override
        public Element get() { return repository.getParentElement(); }
    }
);

Throwable exception = new NoSuchElementException("no sources set up");
for (ElementSource source : sources) {
    try {
        return source.get();
    } catch (NotFoundException e) {
        exception = e;
    }
}
// we end up here if we didn't already return
// so throw the last exception
throw exception;

Cela ne peut être recommandé que si vous disposez réellement d'un grand nombre de sources ou si vous devez configurer les sources au moment de l'exécution. Sinon, c'est une abstraction inutile et vous profiteriez davantage de garder votre code simple et stupide, et d'utiliser simplement ces vilains try-catch imbriqués.

amon
la source
+1 pour mentionner le Trytype dans Scala, pour mentionner les monades et pour la solution utilisant une boucle.
Giorgio
Si j'étais déjà sur Java 8, j'irais pour cela, mais comme vous le dites, c'est un peu trop pour quelques solutions de rechange.
Alex Wittig
1
En fait, au moment où cette réponse a été publiée, Java 8 avec le support de la Optionalmonade ( preuve ) était déjà sorti.
mkalkov
3

Si vous prévoyez que beaucoup de ces appels de référentiel vont être lancés NotFoundException, vous pouvez utiliser un wrapper autour du référentiel pour rationaliser le code. Je ne recommanderais pas cela pour des opérations normales, remarquez:

public class TolerantRepository implements SomeKindOfRepositoryInterfaceHopefully {

    private Repository repo;

    public TolerantRepository( Repository r ) {
        this.repo = r;
    }

    public SomeType getElement( SomeType x ) {
        try {
            return this.repo.getElement(x);
        }
        catch (NotFoundException e) {
            /* For example */
            return null;
        }
    }

    // and the same for other methods...

}
Rory Hunter
la source
3

À la suggestion de @ amon, voici une réponse plus monadique. C'est une version très réduite, où vous devez accepter quelques hypothèses:

  • la fonction "unit" ou "return" est le constructeur de classe

  • l'opération "bind" se produit au moment de la compilation, elle est donc cachée à l'invocation

  • les fonctions "action" sont également liées à la classe au moment de la compilation

  • bien que la classe soit générique et englobe toute classe arbitraire E, je pense que c'est en fait exagéré dans ce cas. Mais je l'ai laissé de cette façon comme exemple de ce que vous pourriez faire.

Avec ces considérations, la monade se traduit par une classe wrapper fluide (bien que vous abandonnez beaucoup de flexibilité que vous obtiendriez dans un langage purement fonctionnel):

public class RepositoryLookup<E> {
    private String source;
    private E answer;
    private Exception exception;

    public RepositoryLookup<E>(String source) {
        this.source = source;
    }

    public RepositoryLookup<E> fetchElement() {
        if (answer != null) return this;
        if (! exception instanceOf NotFoundException) return this;

        try {
            answer = lookup(source);
        }
        catch (Exception e) {
            exception = e;
        }

        return this;
    }

    public RepositoryLookup<E> orFetchSimilarElement() {
        if (answer != null) return this; 
        if (! exception instanceOf NotFoundException) return this;

        try {
            answer = lookupVariation(source);
        }
        catch (Exception e) {
            exception = e;
        }

        return this;
    }

    public RepositoryLookup<E> orFetchParentElement() {
        if (answer != null) return this; 
        if (! exception instanceOf NotFoundException) return this;

        try {
            answer = lookupParent(source);
        }
        catch (Exception e) {
            exception = e;
        }

        return this;
    }

    public boolean failed() {
        return exception != null;
    }

    public Exception getException() {
        return exception;
    }

    public E getAnswer() {
        // better to check failed() explicitly ;)
        if (this.exception != null) {
            throw new IllegalArgumentException(exception);
        }
        // TODO: add a null check here?
        return answer;
    }
}

(cela ne se compilera pas ... certains détails restent inachevés pour garder l'échantillon petit)

Et l'invocation ressemblerait à ceci:

Repository<String> repository = new Repository<String>(x);
repository.fetchElement().orFetchParentElement().orFetchSimilarElement();

if (repository.failed()) {
    throw new IllegalArgumentException(repository.getException());
}

System.err.println("Got " + repository.getAnswer());

Notez que vous avez la possibilité de composer les opérations de "récupération" comme vous le souhaitez. Il s'arrêtera lorsqu'il obtiendra une réponse ou une exception autre que non trouvée.

J'ai fait ça très vite; ce n'est pas tout à fait vrai, mais j'espère que cela transmet l'idée

Rob
la source
1
repository.fetchElement().fetchParentElement().fetchSimilarElement();- à mon avis: code diabolique (au sens donné par Jon Skeet)
Konrad Morawski
certaines personnes n'aiment pas ce style, mais l'utilisation return thispour créer des appels d'objets de chaînage existe depuis longtemps. Puisque OO implique des objets mutables, return thisest plus ou moins équivalent à return nullsans chaînage. Cependant, return new Thing<E>ouvre la porte à une autre capacité que cet exemple n'entre pas, il est donc important pour ce modèle si vous choisissez de suivre cette voie.
Rob
1
Mais j'aime ce style et je ne suis pas contre l'enchaînement d'appels ou les interfaces fluides en tant que tels. Il y a cependant une différence entre CustomerBuilder.withName("Steve").withID(403)et ce code, car rien .fetchElement().fetchParentElement().fetchSimilarElement()qu'en voyant ce n'est pas clair ce qui se passe, et c'est la clé ici. Sont-ils tous récupérés? Ce n'est pas cumulable dans ce cas, et donc pas si intuitif. J'ai besoin de voir ça if (answer != null) return thisavant de vraiment comprendre. C'est peut-être juste une question de nom ( orFetchParent), mais c'est quand même "magique".
Konrad Morawski
1
Soit dit en passant (je sais que votre code est trop simplifié et n'est qu'une preuve de concept), il serait peut-être bon de renvoyer un clone de answerin getAnsweret de réinitialiser (effacer) le answerchamp lui-même avant de renvoyer sa valeur. Sinon, cela rompt en quelque sorte le principe de la séparation commande / requête, car demander à récupérer un élément (interrogation) modifie l'état de votre objet de référentiel ( answern'est jamais réinitialisé) et affecte le comportement fetchElementlors de votre prochain appel. Oui, je suis un peu tatillonne, je pense que la réponse est valable, ce n'est pas moi qui l'ai rejetée.
Konrad Morawski
1
c'est un bon point. Une autre façon serait "tentativeToFetch ...". Le point important est que dans ce cas, les 3 méthodes sont appelées, mais dans un autre cas, un client peut simplement utiliser "tentativeFetch (). TentativeFetchParent ()". En outre, l'appeler "Référentiel" est faux, car il s'agit en fait de modéliser une seule extraction. Je vais peut-être me tromper avec les noms pour l'appeler "RepositoryLookup" et "essayer" de préciser qu'il s'agit d'un artefact transitoire à un coup qui fournit une certaine sémantique autour d'une recherche.
Rob
2

Une autre façon de structurer une série de conditions comme celle-ci est de porter un indicateur, ou bien de tester la valeur null (mieux encore, utilisez l'option facultative de Guava pour déterminer quand une bonne réponse est présente) afin de chaîner les conditions ensemble.

Element e = null;

try {
    e = repository.getElement(x);
} catch (NotFoundException e) {
    // nope -- try again!
}

if (e == null) {  // or ! optionalElement.isPresent()
    try {
        return repository.getSimilarElement(x);
    } catch (NotFoundException e1) {
        // nope -- try again!
    }
}

if (e == null) {  // or ! optionalElement.isPresent()
    try {
        return repository.getParentElement(x);
    } catch (NotFoundException e2) {
        // nope -- try again!
    }
}

if (e == null) {  // or ! optionalElement.isPresent()
    //can't recover
    throw new IllegalArgumentException(e);
}

return e;

De cette façon, vous surveillez l'état de l'élément et effectuez les bons appels en fonction de son état, c'est-à-dire tant que vous n'avez pas encore de réponse.

(Je suis d'accord avec @amon, cependant. Je recommanderais de regarder un modèle Monad, avec un objet wrapper comme class Repository<E>celui-ci qui a des membres E answer;et Exception error;. À chaque étape, vérifiez s'il y a une exception, et si oui, ignorez chaque étape restante. à la fin, vous vous retrouvez avec une réponse, l'absence de réponse ou une exception et vous pouvez décider quoi faire avec cela.)

Rob
la source
-2

Premièrement, il me semble qu'il devrait y avoir une fonction comme repository.getMostSimilar(x)(vous devriez choisir un nom plus approprié) car il semble y avoir une logique qui est utilisée pour trouver l'élément le plus proche ou le plus similaire pour un élément donné.

Le référentiel peut alors implémenter la logique comme indiqué dans amons post. Cela signifie que le seul cas où une exception doit être levée est quand il n'y a aucun élément unique qui pourrait être trouvé.

Cependant, cela n'est bien sûr possible que si les logiques permettant de trouver l'élément le plus proche peuvent être encapsulées dans le référentiel. Si cela n'est pas possible, veuillez fournir plus d'informations sur la manière (selon quels critères) l'élément le plus proche peut être choisi.

valenterry
la source
les réponses sont de répondre à la question, pas de demander des éclaircissements
gnat
Eh bien, ma réponse résout son problème car elle montre un moyen d'éviter les tentatives / captures imbriquées sous certaines conditions. Ce n'est que si ces conditions ne sont pas remplies que nous avons besoin de plus d'informations. Pourquoi n'est-ce pas une réponse valable?
valenterry