alternativas a las capturas de prueba anidadas en caso de fallbacks

13

Tengo una situación en la que estoy intentando recuperar un objeto. Si la búsqueda falla, tengo varias alternativas en su lugar, cada una de las cuales puede fallar. Entonces el código parece:

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

Esto se ve muy feo. Odio volver nulo, pero ¿es mejor en esta situación?

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;

¿Hay otras alternativas?

¿El uso de bloques de captura-prueba anidados es un anti-patrón? está relacionado, pero las respuestas están ahí las líneas de "a veces, pero generalmente es evitable", sin decir cuándo o cómo evitarlo.

    
pregunta Alex Wittig 29.04.2014 - 23:16

6 respuestas

16

La forma habitual de eliminar el anidamiento es usar las funciones:

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 estas reglas de recuperación son universales, podría considerar implementarlo directamente en el objeto repository , donde podría usar solo las declaraciones if en lugar de una excepción.

    
respondido por el Karl Bielefeldt 30.04.2014 - 00:18
12

Esto sería realmente fácil con algo como una mónada de Opción. Desafortunadamente, Java no tiene esos. En Scala, usaría el Try type para encontrar el Primera solución exitosa.

En mi mentalidad de programación funcional, configuré una lista de devoluciones de llamadas que representan las diversas fuentes posibles, y las repasé hasta que encontremos la primera exitosa:

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;

Esto se puede recomendar solo si realmente tiene un gran número de fuentes, o si tiene que configurar las fuentes en tiempo de ejecución. De lo contrario, esto es una abstracción innecesaria y te beneficiarías más si mantienes tu código simple y estúpido, y solo usas esos feos intentos anidados.

    
respondido por el amon 29.04.2014 - 23:36
3

Si está anticipando que muchas de esas llamadas al repositorio van a generar NotFoundException , podría usar un contenedor alrededor del repositorio para simplificar el código. No recomendaría esto para operaciones normales, tenga en cuenta:

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...

}
    
respondido por el Rory Hunter 30.04.2014 - 12:19
3

Por sugerencia de @amon, aquí hay una respuesta que es más monádica. Es una versión muy reducida, en la que hay que aceptar algunas suposiciones:

  • la función "unit" o "return" es el constructor de la clase

  • la operación de "enlace" ocurre en tiempo de compilación, por lo que está oculta de la invocación

  • las funciones de "acción" también están vinculadas a la clase en tiempo de compilación

  • aunque la clase es genérica y envuelve cualquier clase E arbitraria, creo que en realidad es una exageración en este caso. Pero lo dejé así como un ejemplo de lo que podrías hacer.

Con esas consideraciones, la mónada se traduce en una clase de envoltura fluida (aunque está renunciando a la flexibilidad que obtendría en un lenguaje 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;
    }
}

(esto no compilará ... algunos detalles quedan sin terminar para mantener la muestra pequeña)

Y la invocación se vería así:

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

Tenga en cuenta que tiene la flexibilidad para componer las operaciones de "búsqueda" que desee. Se detendrá cuando reciba una respuesta o una excepción que no se encuentre.

Hice esto muy rápido; no está del todo bien, pero espero que transmita la idea

    
respondido por el Rob 30.04.2014 - 00:52
2

Otra forma de estructurar una serie de condiciones como esta es llevar una bandera o, si no, comprobar si hay un valor nulo (mejor aún, utilice la Opción de Guava para determinar cuándo está presente una buena respuesta) para encadenar las condiciones.

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 esa manera, estás observando el estado del elemento y haciendo las llamadas correctas en función de su estado, es decir, mientras no tengas una respuesta todavía.

(Estoy de acuerdo con @amon, sin embargo. Recomiendo mirar un patrón de Monad, con un objeto envoltorio como class Repository<E> que tenga miembros E answer; y Exception error; . En cada etapa, verifique si hay un excepción, y si es así, omita cada paso restante. Al final, le queda una respuesta, la ausencia de una respuesta o una excepción, y puede decidir qué hacer con eso.

    
respondido por el Rob 30.04.2014 - 00:32
-2

Primero, me parece que debería haber una función como repository.getMostSimilar(x) (debes elegir un nombre más apropiado) ya que parece haber una lógica que se usa para encontrar el elemento más cercano o más similar para un elemento dado.

El repositorio puede implementar la lógica como se muestra en la publicación de amons. Eso significa que el único caso en el que se debe lanzar una excepción es cuando no se puede encontrar un elemento único.

Sin embargo, esto, por supuesto, solo es posible si las lógicas para encontrar el elemento más cercano se pueden encapsular en el repositorio. Si esto no es posible, proporcione más información sobre cómo (según qué criterio) se puede elegir el elemento más cercano.

    
respondido por el valenterry 30.04.2014 - 00:15

Lea otras preguntas en las etiquetas