alternativas para capturas de tentativa aninhadas para fallbacks

14

Eu tenho uma situação em que estou tentando recuperar um objeto. Se a pesquisa falhar, tenho vários substitutos, cada um dos quais pode falhar. Portanto, o código se parece com:

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);
        }
    }
}

Isso parece muito feio. Eu odeio retornar nulo, mas é melhor nessa situação?

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;

Existem outras alternativas?

O uso de blocos try-catch aninhados é um anti-padrão? está relacionado, mas as respostas existem no sentido de "às vezes, mas geralmente é evitável", sem dizer quando ou como evitá-lo.

Alex Wittig
fonte
1
É NotFoundExceptionalgo realmente excepcional?
Não sei, e é provavelmente por isso que estou tendo problemas. Isso ocorre em um contexto de comércio eletrônico, onde os produtos são descontinuados diariamente. Se alguém marcar um produto que foi descontinuado posteriormente e tentar abrir o marcador ... isso é excepcional?
Alex Wittig
@ FiveNine na minha opinião, definitivamente não - é de se esperar. Veja stackoverflow.com/questions/729379/…
Konrad Morawski

Respostas:

17

A maneira usual de eliminar o aninhamento é usar funções:

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);
    }
}

Se essas regras de fallback forem universais, considere implementar isso diretamente no repositoryobjeto, onde você poderá usar apenas ifinstruções simples em vez de uma exceção.

Karl Bielefeldt
fonte
1
Nesse contexto, methodseria uma palavra melhor que function.
Sulthan
12

Isso seria realmente fácil com algo como uma mônada da Option. Infelizmente, o Java não os possui. Em Scala, eu usaria o Trytipo para encontrar a primeira solução bem-sucedida.

Na minha mentalidade de programação funcional, eu configurava uma lista de retornos de chamada representando as várias fontes possíveis e as percorria até encontrar a primeira bem-sucedida:

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;

Isso pode ser recomendado apenas se você realmente tiver um grande número de fontes ou se precisar configurá-las em tempo de execução. Caso contrário, essa é uma abstração desnecessária e você lucraria mais se mantivesse seu código simples e estúpido, e apenas usasse essas feias tentativas aninhadas.

amon
fonte
+1 por mencionar o Trytipo em Scala, por mencionar mônadas e pela solução usando um loop.
Giorgio
Se eu já estivesse no Java 8, eu aceitaria isso, mas, como você diz, é um pouco demais para apenas alguns fallbacks.
Alex Wittig
1
Na verdade, quando essa resposta foi publicada, o Java 8 com suporte para a Optionalmônada ( prova ) já estava liberado.
Mkalkov
3

Se você está antecipando que muitas dessas chamadas de repositório serão lançadas NotFoundException, use um wrapper em torno do repositório para otimizar o código. Eu não recomendaria isso para operações normais, lembre-se:

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
fonte
3

Por sugestão da @on, aqui está uma resposta mais monádica. É uma versão bastante simplificada, na qual você deve aceitar algumas suposições:

  • a função "unit" ou "return" é o construtor da classe

  • a operação "bind" acontece no momento da compilação, portanto é oculta da invocação

  • as funções "action" também são vinculadas à classe em tempo de compilação

  • embora a classe seja genérica e envolva qualquer classe arbitrária E, acho que é realmente um exagero nesse caso. Mas deixei assim como um exemplo do que você poderia fazer.

Com essas considerações, a mônada se traduz em uma classe de wrapper fluente (embora você esteja perdendo muita flexibilidade que obteria em uma linguagem puramente funcional):

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;
    }
}

(isso não será compilado ... alguns detalhes são deixados inacabados para manter a amostra pequena)

E a invocação ficaria assim:

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());

Observe que você tem a flexibilidade de compor as operações de "busca" como desejar. Parará quando receber uma resposta ou uma exceção diferente da não encontrada.

Eu fiz isso muito rápido; não está certo, mas espero que transmita a ideia

Roubar
fonte
1
repository.fetchElement().fetchParentElement().fetchSimilarElement();- na minha opinião: Código de mal (no sentido dado por Jon Skeet)
Konrad Morawski
algumas pessoas não gostam desse estilo, mas o uso return thispara criar chamadas de objetos em cadeia existe há muito tempo. Como o OO envolve objetos mutáveis, return thisé mais ou menos equivalente a return nullsem encadeamento. No entanto, return new Thing<E>abre a porta para outro recurso que este exemplo não aborda, por isso é importante para esse padrão se você optar por seguir esse caminho.
Rob
1
Mas eu gosto desse estilo e não sou contra ligações em cadeia ou interfaces fluentes como tal. Há, no entanto, uma diferença entre CustomerBuilder.withName("Steve").withID(403)e esse código, porque, ao vermos .fetchElement().fetchParentElement().fetchSimilarElement(), não está claro o que acontece, e essa é a chave aqui. Todos eles são buscados? Não é acumulativo neste caso e, portanto, não é tão intuitivo. Eu preciso ver isso if (answer != null) return thisantes de realmente entender. Talvez seja apenas uma questão de nomeação adequada ( orFetchParent), mas é "mágica" de qualquer maneira.
precisa saber é o seguinte
1
A propósito (eu sei que seu código é simplificado demais e apenas uma prova de conceito), talvez seja bom retornar um clone de answerin getAnswere redefinir (limpar) o answerpróprio campo antes de retornar seu valor. Caso contrário, isso meio que quebra o princípio da separação de comando / consulta, porque pedir para buscar um elemento (consulta) altera o estado do seu objeto de repositório ( answernunca é redefinido) e afeta o comportamento de fetchElementquando você o chamar da próxima vez. Sim, estou falando um pouco, acho que a resposta é válida, não fui eu quem votou contra.
Konrad Morawski
1
este é um bom ponto. Outra maneira seria "tryToFetch ...". O ponto importante é que, neste caso, todos os três métodos são chamados, mas, em outro caso, um cliente pode apenas usar "tryFetch (). TryFetchParent ()". Além disso, chamá-lo de "Repositório" está errado, porque está realmente modelando uma única busca. Talvez eu deva mexer nos nomes para chamá-lo de "RepositoryLookup" e "tentar" deixar claro que esse é um artefato transitório e único que fornece certa semântica em torno de uma pesquisa.
30514 Rob
2

Outra maneira de estruturar uma série de condições como essa é carregar uma bandeira ou testar se é nulo (melhor ainda, use o Guava's Optional para determinar quando uma boa resposta está presente) para encadear as condições.

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;

Dessa forma, você está observando o estado do elemento e fazendo as chamadas corretas com base em seu estado - ou seja, desde que você ainda não tenha uma resposta.

(Eu concordo com o @amon, no entanto. Eu recomendo olhar para um padrão Mônada, com um objeto wrapper como class Repository<E>esse com membros E answer;e Exception error;. Em cada estágio, verifique se há uma exceção e, se houver, pule cada etapa restante. no final, você fica com uma resposta, a ausência de uma resposta ou uma exceção e pode decidir o que fazer com isso.)

Roubar
fonte
-2

Primeiro, parece-me que deve haver uma função como repository.getMostSimilar(x)(você deve escolher um nome mais apropriado), pois parece haver uma lógica usada para encontrar o elemento mais próximo ou mais semelhante de um determinado elemento.

O repositório pode então implementar a lógica, como mostrado no post de amons. Isso significa que o único caso em que uma exceção deve ser lançada é quando não há um único elemento que possa ser encontrado.

No entanto, isso só é possível se as lógicas para encontrar o elemento mais próximo puderem ser encapsuladas no repositório. Se isso não for possível, forneça mais informações sobre como (por quais critérios) o elemento mais próximo pode ser escolhido.

valenterry
fonte
as respostas são para responder à pergunta, não para solicitar esclarecimentos #
30614
Bem, minha resposta está resolvendo o problema dele, pois mostra uma maneira de evitar as tentativas / capturas aninhadas sob determinadas condições. Somente se essas condições não forem atendidas, precisamos de mais informações. Por que essa resposta não é válida?
valenterry