대체를위한 중첩 된 try-catch에 대한 대안


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
는 IS NotFoundException실제로 특별한 무언가는?

잘 모르겠습니다. 아마 문제가있는 것 같습니다. 전자 상거래 환경에서 제품이 매일 중단됩니다. 누군가가 이후에 단종 된 제품을 북마크 한 다음 북마크를 열려고하면 ... 그 예외입니까?
— Alex Wittig

내 의견으로는 @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입니다.
— Sulthan

12

이것은 옵션 모나드와 같은 것이 정말 쉽습니다. 불행히도 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를 사용하면됩니다.


Try스칼라 에서 유형을 언급하고 모나드를 언급하고 루프를 사용하는 솔루션에 +1합니다 .
— Giorgio

Java 8을 이미 사용하고 있다면 이것으로 갈 것입니다.하지만 당신이 말했듯이, 몇 가지 단점이 있습니다.
— Alex Wittig

1
실제로이 답변이 게시 될 때 Optional모나드 ( proof ) 를 지원하는 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의 제안에서 더 모나 딕 한 답변이 있습니다. 그것은 매우 삶은 버전이며 몇 가지 가정을 받아 들여야합니다.

  • "unit"또는 "return"함수는 클래스 생성자입니다.

  • "바인드"작업은 컴파일시 발생하므로 호출에서 숨겨집니다.

  • "action"함수는 컴파일 타임에 클래스에 바인딩됩니다

  • 클래스는 일반적이며 임의의 클래스 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();-내 의견으로는 : 악한 코드 (존
— 스케이트가

어떤 사람들은 그 스타일을 좋아하지 않지만 return this체인 객체 호출을 만드는 데 오랫동안 사용되어 왔습니다. OO는 변경 가능한 객체를 포함하므로 체인 return this이 return null없는 것과 거의 동일합니다 . 그러나이 return new Thing<E>예제에 포함되지 않은 다른 기능의 문을 열어 주므로이 경로를 따라 가려면이 패턴이 중요합니다.
— Rob

1
그러나 나는 그 스타일을 좋아하고 체인 호출이나 유창한 인터페이스에 반대하지 않습니다. 그러나이 CustomerBuilder.withName("Steve").withID(403)코드와 의 차이점은 .fetchElement().fetchParentElement().fetchSimilarElement()어떤 일이 발생하는지 명확하지 않기 때문에 여기에서 핵심 사항입니다. 그들은 모두 가져옵니다? 이 경우에는 누적되지 않으므로 직관적이지 않습니다. 나는 if (answer != null) return this그것을 정말로 얻기 전에 그것을 볼 필요가 있다. 아마도 적절한 이름 지정 ( orFetchParent) 문제 일 수도 있지만 어쨌든 "마법"입니다.
— Konrad Morawski

1
그건 그렇고 (코드가 지나치게 단순화되고 개념 증명이라는 것을 알고 있습니다), 값을 반환하기 전에 answerin 의 복제본을 반환 getAnswer하고 answer필드 자체를 재설정 (삭제)하는 것이 좋습니다. 그렇지 않으면 요소를 가져 오도록 요청 (쿼리)하면 리포지토리 개체의 상태가 변경되고 ( answer재설정되지 않음) fetchElement다음에 호출 할 때 의 동작에 영향을주기 때문에 일종의 명령 / 쿼리 분리 원칙을 위반합니다 . 예, 조금 따끔 거립니다. 답이 유효하다고 생각합니다.
— Konrad Morawski

1
그건 좋은 지적이야. 또 다른 방법은 "attemptToFetch ..."입니다. 중요한 점은이 경우 3 개의 메소드가 모두 호출되지만 다른 경우에는 클라이언트가 "attemptFetch (). attemptFetchParent ()"를 사용할 수 있다는 것입니다. 또한 "리포지토리"라고 부르는 것은 실제로 단일 페치를 모델링하기 때문에 잘못되었습니다. 어쩌면 "RepositoryLookup"과 "시도"라고 부르는 이름으로 소란스러워서 이것이 조회 주위에 특정 의미를 제공하는 일회성, 일시적인 인공물이라는 것을 분명히합니다.
— Rob

2

이와 같은 일련의 조건을 구성하는 또 다른 방법은 조건을 함께 연결하기 위해 플래그를 운반하거나 null을 테스트하는 것입니다 (아직 더 좋은 대답이 있는지 결정하기 위해 Guava의 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에 동의합니다. class Repository<E>멤버 E answer;와와 같은 래퍼 객체를 사용하여 Monad 패턴을 보는 것이 좋습니다 Exception error;. 각 단계에서 예외가 있는지 확인하고 필요한 경우 나머지 각 단계를 건너 뜁니다. 결국, 당신은 대답, 답이 없거나 예외로 남습니다. 그러면 어떻게 할 것인지 결정할 수 있습니다.)


-2

먼저, 다음과 같은 기능이 있어야합니다. repository.getMostSimilar(x) 주어진 요소에 가장 근접하거나 가장 유사한 요소를 찾는 데 사용되는 논리가있는 것처럼 (더 적절한 이름을 선택해야합니다) 이 있어야합니다.

그런 다음 저장소는 amons post에 표시된 것과 같은 논리를 구현할 수 있습니다. 즉, 예외가 발생해야하는 유일한 경우는 찾을 수있는 단일 요소가없는 경우입니다.

그러나 가장 가까운 요소를 찾는 논리를 저장소에 캡슐화 할 수있는 경우에만 가능합니다. 이것이 가능하지 않은 경우 가장 가까운 요소를 선택하는 방법 (기준에 따라)에 대한 자세한 정보를 제공하십시오.


대답은 명확성을 요구하지 않고 질문에 대답하는 것입니다
— gnat

글쎄, 내 대답은 특정 조건에서 중첩 된 시도 / 캐치를 피하는 방법을 보여주기 때문에 그의 문제를 해결하는 것입니다. 이러한 조건이 충족되지 않는 경우에만 추가 정보가 필요합니다. 이것이 올바른 답변이 아닌 이유는 무엇입니까?
— valenterry
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.