альтернативи вкладеним пробним уловам для резервних копій


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

Це виглядає жахливо потворно. Я ненавиджу повертати нульове значення, але чи краще в цій ситуації?

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;

Чи є інші альтернативи?

Чи використання вкладених блоків пробної застави анти-візерунок? це пов'язано, але відповіді є в рядках "іноді, але зазвичай це можна уникнути", не кажучи, коли або як цього уникнути.


1
Це NotFoundExceptionщось насправді виняткове?

Я не знаю, і це, мабуть, тому у мене проблеми. Це відбувається в контексті електронної комерції, коли продукти відмовляються щодня. Якщо хтось робить закладки на продукт, який згодом припиняється, а потім намагається відкрити закладку ... це винятково?
Алекс Віттіг

@FiveNine, на мою думку, точно ні - цього можна очікувати. Дивіться stackoverflow.com/questions/729379/…
Конрад Моравський

Відповіді:


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.
Султан

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;

Це можна рекомендувати, лише якщо у вас дійсно велика кількість джерел або якщо вам доведеться налаштувати джерела під час виконання. В іншому випадку це непотрібна абстракція, і ви більше заробляте на тому, щоб ваш код був простим і нерозумним, і просто використовуйте ці потворні вкладені спроби.


+1 для згадування Tryтипу в Scala, для згадування монад і для рішення з використанням циклу.
Джорджіо

Якби я вже був на Java 8, я б пішов на це, але, як ви кажете, це трохи більше всього за кілька резервних копій.
Алекс Віттіг

1
Насправді, до моменту опублікування цієї відповіді, Java 8 із підтримкою Optionalмонади ( доказ ) вже була випущена.
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();- на мій погляд: злий код (у тому сенсі, що його надає Джон Скіт)
Конрад Моравський

деяким людям не подобається цей стиль, але використання return thisдля створення ланцюжкових дзвінків об'єктів існує вже давно. Оскільки ОО включає в себе об'єкти, що змінюються, return thisбільш-менш рівнозначно return nullбез прив'язки. Однак return new Thing<E>відкриває двері до іншої можливості, в яку цей приклад не входить, тому для цієї моделі важливо, якщо ви вирішите піти цим шляхом.
Роб

1
Але мені подобається цей стиль, і я не проти ланцюжкових дзвінків або вільних інтерфейсів як таких. Однак існує різниця між CustomerBuilder.withName("Steve").withID(403)цим кодом, тому що лише з .fetchElement().fetchParentElement().fetchSimilarElement()його бачення не зрозуміло, що відбувається, і ось тут головне. Чи всі вони дістаються? У цьому випадку це не накопичувально, а отже, не таке інтуїтивне. Мені потрібно подивитися, if (answer != null) return thisперш ніж я справді це отримаю. Можливо, це лише питання належного іменування ( orFetchParent), але це все одно "магія".
Конрад Моравський

1
До речі (я знаю, що ваш код надто спрощений і є лише доказом концепції), було б добре, можливо, повернути клон answerв getAnswerі скинути (очистити) саме answerполе, перш ніж повернути його значення. В іншому випадку він начебто порушує принцип розділення команд / запитів, тому що прохання отримати елемент (запит) змінює стан вашого об'єкта сховища ( answerніколи не скидається) і впливає на поведінку, fetchElementколи ви його дзвоните наступного разу. Так, я трохи задумуюсь, я вважаю, що відповідь достовірна, я не був тим, хто виступав проти цього.
Конрад Моравський

1
це хороший момент. Іншим способом було б "спробаToFetch ...". Важливим моментом є те, що в цьому випадку викликаються всі 3 методи, але в іншому випадку клієнт може просто використовувати "tryFetch (). СпробаFetchParent ()". Крім того, називати його "сховищем" помилково, тому що це дійсно моделювання одного вибору. Можливо, я буду метушитися з іменами, щоб назвати це "RepositoryLookup" та "спроба" зрозуміти, що це одноразовий, тимчасовий артефакт, який забезпечує певну семантику навколо пошуку.
Роб

2

Інший спосіб структурувати такі умови, як ця, - це перенесення прапора або тест на нуль (ще краще, використовуйте необов'язковий Guava, щоб визначити, коли є хороша відповідь), щоб зв'язати умови разом.

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. Це означає, що єдиний випадок, який слід викинути, - це коли немає жодного елемента, який можна було б знайти.

Однак це, звичайно, можливо лише в тому випадку, якщо логіка для пошуку найближчого елемента може бути інкапсульована в сховище. Якщо це неможливо, будь ласка, надайте більше інформації про те, як (за якими критеріями) можна вибрати найближчий елемент.


відповіді - відповісти на запитання, а не вимагати роз'яснень
gnat

Ну, моя відповідь - це вирішення його проблеми, оскільки це показує спосіб уникнути вкладених спроб / уловлень за певних умов. Тільки якщо ці умови не виконані, нам потрібна додаткова інформація. Чому це неправдива відповідь?
валентери
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.