alternatif untuk try-catch bersarang untuk fallback


14

Saya memiliki situasi di mana saya mencoba mengambil objek. Jika pencarian gagal, saya memiliki beberapa fallback di tempat, yang masing-masing mungkin gagal. Jadi kodenya terlihat seperti:

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

Ini terlihat sangat jelek. Saya benci untuk mengembalikan nol, tetapi apakah itu lebih baik dalam situasi ini?

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;

Apakah ada alternatif lain?

Apakah menggunakan blok try-catch bersarang merupakan anti-pola? terkait, tetapi jawabannya ada di sepanjang baris "kadang-kadang, tetapi biasanya dapat dihindari", tanpa mengatakan kapan atau bagaimana cara menghindarinya.


1
Apakah NotFoundExceptionsesuatu yang benar-benar luar biasa?

Saya tidak tahu, dan mungkin itulah sebabnya saya mengalami masalah. Ini dalam konteks e-niaga, di mana produk dihentikan setiap hari. Jika seseorang membookmark produk yang kemudian dihentikan, dan kemudian mencoba untuk membuka bookmark ... apakah itu luar biasa?
Alex Wittig

@FiveNine menurut pendapat saya, pasti tidak - itu yang diharapkan. Lihat stackoverflow.com/questions/729379/...
Konrad Morawski

Jawaban:


17

Cara biasa untuk menghilangkan sarang adalah menggunakan fungsi:

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

Jika aturan fallback ini bersifat universal, Anda bisa mempertimbangkan untuk menerapkan ini secara langsung di repositoryobjek, di mana Anda mungkin bisa menggunakan ifpernyataan sederhana alih-alih pengecualian.


1
Dalam konteks ini, methodakan menjadi kata yang lebih baik daripada function.
Sulthan

12

Ini akan sangat mudah dengan sesuatu seperti Opsi monad. Sayangnya, Java tidak memilikinya. Di Scala, saya akan menggunakan Trytipe untuk menemukan solusi sukses pertama.

Dalam pola pikir fungsional-pemrograman saya, saya akan membuat daftar panggilan balik yang mewakili berbagai sumber yang mungkin, dan mengulanginya sampai kita menemukan yang sukses pertama:

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;

Ini dapat direkomendasikan hanya jika Anda benar-benar memiliki sejumlah besar sumber, atau jika Anda harus mengkonfigurasi sumber saat runtime. Jika tidak, ini adalah abstraksi yang tidak perlu dan Anda akan mendapat lebih banyak manfaat dari menjaga kode Anda sederhana dan bodoh, dan gunakan saja hasil tangkapan yang jelek itu.


+1 untuk menyebutkan Tryjenis dalam Scala, untuk menyebutkan monad, dan untuk solusi menggunakan loop.
Giorgio

Jika saya berada di Java 8 saya sudah akan pergi untuk ini, tetapi seperti yang Anda katakan, itu sedikit banyak hanya untuk beberapa mundur.
Alex Wittig

1
Sebenarnya, pada saat jawaban ini diposting, Java 8 dengan dukungan untuk Optionalmonad ( bukti ) sudah dirilis.
mkalkov

3

Jika Anda mengantisipasi bahwa banyak panggilan repositori akan dilemparkan NotFoundException, Anda bisa menggunakan pembungkus di sekitar repositori untuk merampingkan kode. Saya tidak akan merekomendasikan ini untuk operasi normal, ingatlah:

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

Atas saran @ amon, inilah jawaban yang lebih monadik. Ini adalah versi yang sangat matang, di mana Anda harus menerima beberapa asumsi:

  • fungsi "unit" atau "return" adalah konstruktor kelas

  • operasi "bind" terjadi pada waktu kompilasi, jadi itu disembunyikan dari doa

  • fungsi "action" juga terikat ke kelas pada waktu kompilasi

  • walaupun kelasnya generik dan membungkus sembarang kelas E, saya pikir itu sebenarnya berlebihan dalam hal ini. Tetapi saya membiarkannya sebagai contoh dari apa yang dapat Anda lakukan.

Dengan pertimbangan tersebut, monad diterjemahkan ke dalam kelas pembungkus yang lancar (meskipun Anda memberikan banyak fleksibilitas yang akan Anda dapatkan dalam bahasa yang murni fungsional):

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

(ini tidak akan dikompilasi ... detail tertentu dibiarkan belum selesai untuk menjaga sampel tetap kecil)

Dan doa akan terlihat seperti ini:

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

Perhatikan bahwa Anda memiliki fleksibilitas untuk menyusun operasi "ambil" sesuka Anda. Itu akan berhenti ketika mendapat jawaban atau pengecualian selain tidak ditemukan.

Saya melakukan ini dengan sangat cepat; itu tidak benar, tetapi mudah-mudahan menyampaikan gagasan itu


1
repository.fetchElement().fetchParentElement().fetchSimilarElement();- menurut saya: kode jahat (dalam pengertian yang diberikan oleh Jon Skeet)
Konrad Morawski

beberapa orang tidak suka gaya itu, tetapi menggunakan return thisuntuk membuat objek panggilan rantai telah ada sejak lama. Karena OO melibatkan objek yang bisa berubah, return thiskurang lebih setara dengan return nulltanpa rantai. Namun, return new Thing<E>buka pintu ke kemampuan lain yang tidak dicontoh oleh contoh ini, jadi penting untuk pola ini jika Anda memilih untuk menempuh jalan ini.
Rob

1
Tapi saya suka gaya itu dan saya tidak menentang panggilan chaining atau antarmuka yang lancar seperti itu. Namun ada perbedaan antara CustomerBuilder.withName("Steve").withID(403)dan kode ini, karena hanya dari melihat .fetchElement().fetchParentElement().fetchSimilarElement()itu tidak jelas apa yang terjadi, dan itulah kuncinya di sini. Apakah mereka semua dijemput? Ini tidak akumulatif dalam kasus ini, dan karena itu tidak intuitif. Saya perlu melihat itu if (answer != null) return thissebelum saya benar-benar mendapatkannya. Mungkin itu hanya masalah penamaan yang tepat ( orFetchParent), tapi itu sih "sihir".
Konrad Morawski

1
Ngomong-ngomong (saya tahu kode Anda terlalu disederhanakan dan hanya bukti konsep), akan lebih baik untuk mengembalikan klon answermasuk getAnswerdan mengatur ulang (hapus) answerbidang itu sendiri sebelum mengembalikan nilainya. Kalau tidak, ini semacam melanggar prinsip pemisahan perintah / kueri, karena meminta untuk mengambil elemen (permintaan) mengubah keadaan objek repositori Anda ( answertidak pernah diatur ulang) dan memengaruhi perilaku fetchElementketika Anda menyebutnya waktu berikutnya. Ya, saya sedikit rewel, saya pikir jawabannya valid, saya bukan orang yang menurunkannya.
Konrad Morawski

1
itu poin yang bagus. Cara lain adalah "percobaanToFetch ...". Poin penting adalah bahwa dalam kasus ini, ketiga metode dipanggil, tetapi dalam kasus lain klien mungkin hanya menggunakan "tryFetch (). EffortFetchParent ()". Juga, menyebutnya "Repositori" salah, karena itu benar-benar memodelkan pengambilan tunggal. Mungkin saya akan repot-repot dengan nama-nama untuk menyebutnya "RepositoryLookup" dan "upaya" untuk memperjelas ini adalah artefak sementara, sekali pakai yang menyediakan semantik tertentu di sekitar pencarian.
Rob

2

Cara lain untuk menyusun serangkaian kondisi seperti ini adalah dengan membawa bendera, atau tes nol (lebih baik, gunakan Pilihan Guava untuk menentukan kapan ada jawaban yang baik) untuk menyatukan kondisi.

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;

Dengan begitu, Anda memperhatikan status elemen, dan melakukan panggilan yang benar berdasarkan kondisinya - yaitu, selama Anda belum memiliki jawaban.

(Tapi saya setuju dengan @amon. Saya akan merekomendasikan melihat pola Monad, dengan objek pembungkus seperti class Repository<E>yang memiliki anggota E answer;dan Exception error;. Pada setiap tahap periksa untuk melihat apakah ada pengecualian, dan jika demikian, lewati setiap langkah yang tersisa. Di akhirnya, Anda memiliki jawaban, tidak ada jawaban, atau pengecualian & Anda dapat memutuskan apa yang harus dilakukan dengan itu.)


-2

Pertama, menurut saya harus ada fungsi seperti repository.getMostSimilar(x)(Anda harus memilih nama yang lebih tepat) karena tampaknya ada logika yang digunakan untuk menemukan elemen terdekat atau paling mirip untuk elemen tertentu.

Repositori kemudian dapat mengimplementasikan logika seperti yang ditunjukkan pada post amons. Itu berarti, satu-satunya kasus pengecualian harus dilemparkan adalah ketika tidak ada elemen tunggal yang dapat ditemukan.

Namun ini tentu saja hanya mungkin jika logika untuk menemukan elemen terdekat dapat dienkapsulasi ke dalam repositori. Jika ini tidak memungkinkan, berikan informasi lebih lanjut bagaimana (dengan kriteria apa) elemen terdekat dapat dipilih.


jawabannya adalah untuk menjawab pertanyaan, bukan untuk meminta klarifikasi
nyamuk

Jawaban saya adalah memecahkan masalahnya karena menunjukkan cara untuk menghindari percobaan bersarang dalam kondisi tertentu. Hanya jika kondisi ini tidak terpenuhi kami memerlukan informasi lebih lanjut. Mengapa ini bukan jawaban yang valid?
valenterri
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.