web-dev-qa-db-ja.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);
        }
    }
}

これはひどく醜く見えます。私はnullを返すのが嫌いですが、この状況ではそれがより良いですか?

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;

他の選択肢はありますか?

ネストされたtry-catchを使用するとアンチパターンがブロックされますか? は関連していますが、その答えは「ときどきですが、通常は回避できます」であり、回避するタイミングや方法は述べていません。

14
Alex Wittig

ネストを排除する通常の方法は、関数を使用することです。

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

これらのフォールバックルールが普遍的なものである場合、これをrepositoryオブジェクトに直接実装することを検討できます。例外の代わりに単純なifステートメントを使用できる場合があります。

17
Karl Bielefeldt

これはOptionモナドのようなもので本当に簡単でしょう。残念ながら、Javaにはそれらがありません。Scalaでは Try type を使用して、最初に成功したソリューションを見つけます。

私の関数型プログラミングの考え方では、考えられるさまざまなソースを表すコールバックのリストを設定し、最初の成功するソースが見つかるまでループします。

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;

これは、実際に多数のソースがある場合、または実行時にソースを構成する必要がある場合にのみ推奨できます。それ以外の場合、これは不必要な抽象化であり、コードを単純で愚かに保つことでさらに利益を得て、醜いネストされたtry-catchを使用するだけです。

12
amon

@amonの提案で、よりモナド的な答えがここにあります。これは非常に煮詰められたバージョンであり、いくつかの仮定を受け入れる必要があります。

  • 「ユニット」または「リターン」関数はクラスコンストラクタです。

  • 「バインド」操作はコンパイル時に行われるため、呼び出しから隠されます

  • 「アクション」関数もコンパイル時にクラスにバインドされます

  • クラスはジェネリックであり、任意のクラスEをラップしますが、この場合は実際にはやりすぎだと思います。しかし、私はあなたができることの例としてそれをそのままにしました。

これらの考慮事項により、モナドは流暢なラッパークラスに変換されます(ただし、純粋に関数型の言語で得られる柔軟性の多くをあきらめています)。

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

(これはコンパイルされません...サンプルを小さく保つために、特定の詳細は未完成のままです)

そして、呼び出しは次のようになります。

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

必要に応じて「フェッチ」操作を柔軟に構成できることに注意してください。見つからない以外の応答または例外を取得すると停止します。

私はこれを本当に速くやった。それは完全に正しくはありませんが、うまくいけばアイデアを伝えます

3
Rob

これらのリポジトリー呼び出しの多くがNotFoundExceptionをスローすると予想している場合は、リポジトリーのラッパーを使用してコードを簡素化できます。通常の操作ではこれをお勧めしません。

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

}
3
Rory Hunter

このような一連の条件を構造化する別の方法は、フラグを実行するか、条件を連鎖させるためにnullをテストすることです(さらに良い場合は、グアバのオプションを使用して適切な回答が存在するかどうかを判断します)。

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;

このようにして、要素の状態を監視し、その状態に基づいて適切な呼び出しを行います。つまり、まだ回答がない限りです。

(ただし、@ amonに同意します。class Repository<E>などのメンバーE answer;Exception error;のラッパーオブジェクトを使用して、モナドパターンを確認することをお勧めします。各ステージで確認する例外がある場合は、残りの各ステップをスキップします。最後に、回答、回答がない、または例外のいずれかが残り、それをどうするかを決定できます。)

2
Rob