フォールバック用のネストされたトライキャッチの代替


14

オブジェクトを取得しようとしている状況があります。ルックアップが失敗した場合、フォールバックがいくつかあり、それぞれが失敗する可能性があります。したがって、コードは次のようになります。

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ブロックの使用はアンチパターンですか?関連しているが、そこにある答えは「いつか、しかし通常は避けることができる」という線に沿っており、いつ、どのようにそれを避けるべきかは述べていない。


1
NotFoundException、実際に例外的である何かが?

私は知りません、そしてそれはおそらく私が問題を抱えている理由です。これは、製品が毎日廃止されるeコマースのコンテキストです。誰かがその後廃止された製品をブックマークし、その後ブックマークを開こうとすると...例外的ですか?
アレックスウィッティヒ14

私の意見では@FiveNine、間違いなくいいえ-それは予想されることです。参照してくださいstackoverflow.com/questions/729379/...
コンラートMorawski

回答:


17

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

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。例外の代わりに単純なステートメントを使用できる場合があります。


1
この文脈でmethodは、がより良い言葉になりfunctionます。
スルタン14

12

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

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

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を使用することでより多くの利益を得られます。


+ 1 Try、Scalaでの型の言及、モナドの言及、およびループを使用した解決策。
ジョルジオ14

私がすでにJava 8を使用していた場合は、この方法を選択しますが、ご指摘のように、いくつかのフォールバックには少し時間がかかります。
アレックスウィッティヒ14

1
実際、この回答が投稿されるまでに、Optionalモナド(証拠)をサポートするJava 8 はすでにリリースされています。
mkalkov

3

これらのリポジトリー呼び出しの多くがスローされると予想している場合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

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

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

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


1
repository.fetchElement().fetchParentElement().fetchSimilarElement();-私の意見では:邪悪なコード(ジョンスキートによって与えられた意味で)
コンラッド・モラウスキー14

一部の人々はそのスタイルが好きではありませんが、return thisオブジェクト呼び出しの連鎖を作成するために使用することは長い間存在していました。OOは可変オブジェクトを含むため、チェーンなしとreturn thisほぼ同等return nullです。ただし、return new Thing<E>この例では説明されていない別の機能への扉が開かれているため、このパターンを進むことを選択した場合、このパターンにとって重要です。
ロブ14

1
しかし、私はそのスタイルが好きで、呼び出しの連鎖や流interfacesなインターフェイス自体には反対ではありません。ただし、CustomerBuilder.withName("Steve").withID(403)このコードとの間に違いがあります。見ただけでは.fetchElement().fetchParentElement().fetchSimilarElement()何が起こるかが明確ではないため、ここで重要なことです。それらはすべて取得されますか?この場合は累積的ではないため、直感的ではありません。if (answer != null) return this本当に得る前にそれを見る必要があります。おそらく適切な命名(orFetchParent)の問題だけかもしれませんが、とにかく「魔法」です。
コンラッドモラウスキ14

1
ちなみに(コードが単純化されており、単なる概念実証であることがわかっています)、answerinのクローンを返し、値を返す前にフィールド自体getAnswerをリセット(クリア)するanswerことをお勧めします。そうしないと、要素のフェッチ(クエリ)を要求するとリポジトリオブジェクトの状態が変更され(answerリセットされない)、fetchElement次回呼び出したときの動作に影響するため、コマンド/クエリの分離の原則が崩れます。はい、私は少し選び抜いています。答えは有効だと思います。私はそれを否定した人ではありませんでした。
コンラッドモラウスキ14

1
それは良い点です。別の方法は「attemptToFetch ...」です。重要な点は、この場合、3つのメソッドすべてが呼び出されることですが、別の場合では、クライアントは「attemptFetch()。attemptFetchParent()」を使用するだけです。また、「リポジトリ」と呼ぶのは間違っています。なぜなら、実際には単一のフェッチをモデリングしているからです。これを「RepositoryLookup」および「attempt」と呼ぶ名前で大騒ぎして、これがルックアップに関する特定のセマンティクスを提供するワンショットの一時的なアーティファクトであることを明確にすることができます。
ロブ14

2

このような一連の条件を構成する別の方法は、フラグを保持するか、条件を連結するためにnullをテストすることです(さらに良いのは、Guava's Optionalを使用して適切な回答が存在するかどうかを判断することです)。

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に同意します。Monadパターンを見て、class Repository<E>メンバーE answer;とを持つラッパーオブジェクトを使用することをお勧めしますException error;。各段階で例外があるかどうかを確認し、そうであれば、残りの各ステップをスキップします。最後に、答え、答えの欠如、または例外のいずれかが残ります。それをどうするかを決定できます。


-2

まず、repository.getMostSimilar(x)特定の要素に最も近いまたは最も類似した要素を見つけるために使用されるロジックがあるように見えるため、(より適切な名前を選択する必要があります)のような関数があるはずです。

その後、リポジトリはamonsポストに示されているようなロジックを実装できます。つまり、例外をスローする必要があるのは、検出できる要素が1つもない場合だけです。

ただし、これはもちろん、最も近い要素を見つけるロジックをリポジトリにカプセル化できる場合にのみ可能です。これが不可能な場合は、最も近い要素をどのように(どの基準で)選択できるかについて、より多くの情報を提供してください。


答えはない、要求の明確化のために、質問に答えるためにある
ブヨ

私の答えは、特定の条件下でネストされたtry / catchesを回避する方法を示しているため、彼の問題を解決することです。これらの条件が満たされない場合にのみ、さらに情報が必要です。なぜこれが有効な答えではないのですか?
ヴァレンテリー14
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.