Apakah melempar pengecualian sebagai anti-pola di sini?

33

Saya baru saja berdiskusi tentang pilihan desain setelah ulasan kode. Saya ingin tahu apa pendapat Anda.

Ada Preferenceskelas ini , yang merupakan ember untuk pasangan nilai kunci. Nilai kosong adalah legal (itu penting). Kami berharap bahwa nilai-nilai tertentu mungkin belum disimpan, dan kami ingin menangani kasus-kasus ini secara otomatis dengan menginisialisasi mereka dengan nilai default yang telah ditentukan saat diminta.

Solusi yang dibahas menggunakan pola berikut (CATATAN: ini bukan kode aktual, jelas - ini disederhanakan untuk tujuan ilustrasi):

public class Preferences {
    // null values are legal
    private Map<String, String> valuesFromDatabase;

    private static Map<String, String> defaultValues;

    class KeyNotFoundException extends Exception {
    }

    public String getByKey(String key) {
        try {
            return getValueByKey(key);
        } catch (KeyNotFoundException e) {
            String defaultValue = defaultValues.get(key);
            valuesFromDatabase.put(key, defaultvalue);
            return defaultValue;
        }
    }

    private String getValueByKey(String key) throws KeyNotFoundException {
        if (valuesFromDatabase.containsKey(key)) {
            return valuesFromDatabase.get(key);
        } else {
            throw new KeyNotFoundException();
        }
    }
}

Itu dikritik sebagai anti-pola - pengecualian menyalahgunakan untuk mengontrol aliran . KeyNotFoundException- dihidupkan hanya untuk satu kasus penggunaan saja - tidak akan pernah terlihat di luar lingkup kelas ini.

Ini pada dasarnya adalah dua metode bermain ambil dengannya hanya untuk berkomunikasi sesuatu satu sama lain.

Kunci yang tidak ada dalam database bukanlah sesuatu yang mengkhawatirkan, atau luar biasa - kami berharap ini terjadi setiap kali pengaturan preferensi baru ditambahkan, karenanya mekanisme yang dengan anggun menginisialisasi dengan nilai default jika diperlukan.

Argumen adalah bahwa getValueByKey- metode swasta - seperti yang didefinisikan sekarang tidak memiliki cara alami menginformasikan metode masyarakat tentang kedua nilai, dan apakah kunci itu ada. (Jika tidak, itu harus ditambahkan agar nilainya dapat diperbarui).

Pengembalian nullakan ambigu, karena nullmerupakan nilai legal sempurna, jadi tidak ada yang tahu apakah itu berarti bahwa kuncinya tidak ada di sana, atau jika ada null.

getValueByKeyharus mengembalikan semacam a Tuple<Boolean, String>, bool diatur ke true jika kuncinya sudah ada, sehingga kita dapat membedakan antara (true, null)dan (false, null). ( outParameter dapat digunakan dalam C #, tapi itu Java).

Apakah ini alternatif yang lebih baik? Ya, Anda harus mendefinisikan beberapa kelas penggunaan tunggal untuk efek Tuple<Boolean, String>, tetapi kemudian kita menyingkirkan KeyNotFoundException, sehingga saldo keluar. Kami juga menghindari overhead penanganan pengecualian, meskipun tidak signifikan dalam hal praktis - tidak ada pertimbangan kinerja untuk dibicarakan, ini adalah aplikasi klien dan tidak seperti preferensi pengguna akan diambil jutaan kali per detik.

Variasi dari pendekatan ini dapat menggunakan Jambu Biji Optional<String>(Jambu sudah digunakan di seluruh proyek) alih-alih beberapa kebiasaan Tuple<Boolean, String>, dan kemudian kita dapat membedakan antara Optional.<String>absent()dan "tepat" null. Meski begitu, masih terasa retas karena alasan yang mudah dilihat - memperkenalkan dua tingkat "ketidakberesan" tampaknya menyalahgunakan konsep yang berdiri di belakang penciptaan Optionaldi tempat pertama.

Pilihan lain adalah dengan memeriksa secara eksplisit apakah kunci tersebut ada (tambahkan boolean containsKey(String key)metode dan panggil getValueByKeysaja jika kami telah menyatakan bahwa kunci itu ada).

Akhirnya, kita juga bisa sebaris metode pribadi, tetapi sebenarnya getByKeyagak lebih kompleks daripada sampel kode saya, sehingga inlining akan membuatnya terlihat sangat buruk.

Saya mungkin membelah rambut di sini, tapi saya ingin tahu apa yang Anda bertaruh untuk menjadi yang paling dekat dengan praktik terbaik dalam kasus ini. Saya tidak menemukan jawaban dalam panduan gaya Oracle atau Google.

Apakah menggunakan pengecualian seperti dalam contoh kode merupakan anti-pola, atau apakah itu dapat diterima mengingat bahwa alternatif juga tidak terlalu bersih? Jika ya, dalam keadaan apa akan baik-baik saja? Dan sebaliknya?

Konrad Morawski
sumber
1
@ GlenH7 jawaban yang diterima (dan yang terpilih tertinggi) meringkas bahwa "Anda harus menggunakannya [pengecualian] dalam kasus ketika mereka membuat penanganan kesalahan lebih mudah dengan lebih sedikit kekacauan kode". Saya kira saya bertanya di sini apakah alternatif yang saya daftarkan harus dianggap "kurang kekacauan kode".
Konrad Morawski
1
Saya menarik VTC saya - Anda meminta sesuatu yang berkaitan dengannya, tetapi berbeda dari duplikat yang saya sarankan.
3
Saya pikir pertanyaannya menjadi lebih menarik ketika getValueByKeyjuga publik.
Doc Brown
2
Untuk referensi di masa mendatang, Anda melakukan hal yang benar. Ini akan menjadi off topic pada Peninjauan Kode karena itu adalah kode contoh.
RubberDuck
1
Hanya untuk berbunyi --- ini adalah Python idiomatis, dan (saya pikir) akan menjadi cara yang lebih disukai untuk menangani masalah ini dalam bahasa itu. Namun, jelaslah, bahasa yang berbeda memiliki konvensi yang berbeda.
Patrick Collins

Jawaban:

75

Ya, kolega Anda benar: itu kode yang buruk. Jika kesalahan dapat ditangani secara lokal, maka itu harus ditangani segera. Pengecualian tidak harus dibuang dan kemudian ditangani segera.

Ini jauh lebih bersih daripada versi Anda ( getValueByKey()metode ini dihapus):

public String getByKey(String key) {
    if (valuesFromDatabase.containsKey(key)) {
        return valuesFromDatabase.get(key);
    } else {
        String defaultValue = defaultValues.get(key);
        valuesFromDatabase.put(key, defaultvalue);
        return defaultValue;
    }
}

Pengecualian harus dibuang hanya jika Anda tidak tahu cara menyelesaikan kesalahan secara lokal.

BЈовић
sumber
14
Terima kasih atas jawabannya. Sebagai catatan, saya adalah kolega ini (saya tidak suka kode aslinya), saya hanya mengutarakan pertanyaan secara netral sehingga tidak dimuat :)
Konrad Morawski
59
Tanda pertama kegilaan: Anda mulai berbicara pada diri sendiri.
Robert Harvey
1
@ BЈовић: baik, dalam hal ini saya pikir tidak apa-apa, tetapi bagaimana jika Anda membutuhkan dua varian - satu metode mengambil nilai tanpa autocreation kunci yang hilang, dan satu lagi dengan? Apa yang menurut Anda merupakan alternatif terbersih (dan KERING)?
Doc Brown
3
@RobertHarvey :) orang lain menulis kode ini, orang yang benar-benar terpisah, saya adalah peninjau
Konrad Morawski
1
@ Alvaro, sering menggunakan pengecualian tidak masalah. Menggunakan pengecualian dengan buruk adalah masalah, terlepas dari bahasa.
kdbanman
11

Saya tidak akan menyebut penggunaan Pengecualian ini sebagai anti-pola, hanya saja bukan solusi terbaik untuk masalah mengkomunikasikan hasil yang kompleks.

Solusi terbaik (dengan asumsi Anda masih menggunakan Java 7) adalah dengan menggunakan Pilihan Guava; Saya tidak setuju bahwa penggunaannya dalam kasus ini akan menjadi peretasan. Menurut saya, berdasarkan penjelasan yang diperluas dari Guava tentang Opsional , bahwa ini adalah contoh sempurna kapan menggunakannya. Anda membedakan antara "tidak ada nilai yang ditemukan" dan "nilai null ditemukan".

Mike Partridge
sumber
1
Saya tidak berpikir Optionaldapat menyimpan null. Optional.of()hanya mengambil referensi bukan-nol dan Optional.fromNullable()memperlakukan nol sebagai "nilai tidak ada".
Navin
1
@Navin tidak dapat menyimpan null, tetapi Anda miliki Optional.absent()siap membantu Anda. Jadi, Optional.fromNullable(string)akan sama dengan Optional.<String>absent()jika stringnull, atau Optional.of(string)jika tidak.
Konrad Morawski
@KonradMorawski Saya pikir masalah dalam OP adalah bahwa Anda tidak dapat membedakan antara string nol dan string yang tidak ada tanpa melemparkan pengecualian. Optional.absent()mencakup salah satu skenario ini. Bagaimana Anda mewakili yang lain?
Navin
@Navin dengan aktual null.
Konrad Morawski
4
@KonradMorawski: Kedengarannya itu ide yang sangat buruk. Saya hampir tidak dapat memikirkan cara yang lebih jelas untuk mengatakan "metode ini tidak pernah kembali null!" daripada menyatakannya sebagai kembali Optional<...>. . .
ruakh
8

Karena tidak ada pertimbangan kinerja dan ini adalah detail implementasi, pada akhirnya tidak masalah solusi mana yang Anda pilih. Tapi saya harus setuju itu gaya buruk; kuncinya adalah absen adalah sesuatu yang Anda tahu akan terjadi, dan Anda bahkan tidak menanganinya lebih dari satu memanggil stack, yang mana pengecualian sangat berguna.

Pendekatan tuple agak hacky karena bidang kedua tidak ada artinya ketika boolean salah. Memeriksa apakah kunci ada sebelumnya konyol karena peta mencari kunci dua kali. (Yah, Anda sudah melakukan itu, jadi dalam kasus ini tiga kali.) OptionalSolusinya sangat cocok untuk masalah tersebut. Mungkin agak ironis untuk menyimpan nulldalam Optional, tetapi jika itu yang ingin dilakukan pengguna, Anda tidak bisa mengatakan tidak.

Seperti dicatat oleh Mike dalam komentar, ada masalah dengan ini; baik Guava maupun Java 8 tidak Optionalmengizinkan penyimpanan null. Dengan demikian, Anda perlu menggulung sendiri yang - meski sederhana - melibatkan pelat ketel yang cukup banyak, jadi mungkin akan terlalu banyak untuk sesuatu yang hanya akan digunakan sekali secara internal. Anda juga dapat mengubah tipe peta Map<String, Optional<String>>, tetapi penanganannya Optional<Optional<String>>menjadi canggung.

Kompromi yang masuk akal mungkin untuk menjaga pengecualian, tetapi mengakui perannya sebagai "pengembalian alternatif". Buat pengecualian yang baru diperiksa dengan jejak stack dinonaktifkan, dan lemparkan instance yang sudah dibuat sebelumnya yang dapat Anda simpan dalam variabel statis. Ini murah - mungkin semurah cabang lokal - dan Anda tidak bisa lupa untuk menanganinya, sehingga kurangnya jejak tumpukan tidak menjadi masalah.

Doval
sumber
1
Ya, pada kenyataannya itu hanya Map<String, String>pada tingkat tabel database, karena valueskolom harus dari satu jenis. Ada lapisan DAO bungkus yang tipe-kuat setiap pasangan nilai kunci. Tapi saya menghilangkan ini untuk kejelasan. (Tentu saja Anda bisa memiliki tabel satu baris dengan n kolom, tetapi kemudian menambahkan setiap nilai baru memerlukan memperbarui skema tabel, jadi menghapus kompleksitas yang terkait dengan harus menguraikannya datang dengan biaya memperkenalkan kompleksitas lain di tempat lain).
Konrad Morawski
Poin bagus tentang tuple. Memang, apa yang (false, "foo")seharusnya berarti?
Konrad Morawski
Setidaknya dengan Guava's Opsional, mencoba untuk menempatkan hasil nol dalam pengecualian. Anda harus menggunakan Optional.abent ().
Mike Partridge
@ MikePartridge Poin bagus. Solusi buruk adalah mengubah peta Map<String, Optional<String>>dan kemudian Anda akan berakhir dengan Optional<Optional<String>>. Solusi yang kurang membingungkan adalah dengan menggulung Opsional Anda sendiri yang memungkinkan null, atau tipe data aljabar serupa yang melarang nulltetapi memiliki 3 status - tidak ada, nol, atau ada. Keduanya mudah diterapkan, meskipun jumlah pelat yang terlibat cukup tinggi untuk sesuatu yang hanya akan digunakan secara internal.
Doval
2
"Karena tidak ada pertimbangan kinerja dan ini adalah detail implementasi, pada akhirnya tidak masalah solusi mana yang Anda pilih" - Kecuali bahwa jika Anda menggunakan C # menggunakan pengecualian akan mengganggu siapa saja yang mencoba men-debug dengan "Istirahat ketika pengecualian dilemparkan "dihidupkan untuk pengecualian CLR.
Stephen
5

Ada kelas Preferensi ini, yang merupakan ember untuk pasangan nilai kunci. Nilai kosong adalah legal (itu penting). Kami berharap bahwa nilai-nilai tertentu mungkin belum disimpan, dan kami ingin menangani kasus-kasus ini secara otomatis dengan menginisialisasi mereka dengan nilai default yang telah ditentukan ketika diminta.

Masalahnya persis seperti ini. Tapi Anda sudah memposting solusinya:

Variasi dari pendekatan ini dapat menggunakan Jambu Opsional (Jambu sudah digunakan di seluruh proyek) alih-alih beberapa Tuple kustom, dan kemudian kita dapat membedakan antara Optional.abent () dan "tepat" nol . Meski begitu, masih terasa retas karena alasan yang mudah dilihat - memperkenalkan dua tingkat "ketidakberesan" tampaknya menyalahgunakan konsep yang berdiri di belakang menciptakan Opsional di tempat pertama.

Namun, jangan gunakan null atau Optional . Anda hanya dapat dan harus menggunakannya Optional. Untuk perasaan "retas" Anda, gunakan saja itu bersarang, sehingga Anda berakhir dengan Optional<Optional<String>>yang membuatnya eksplisit bahwa mungkin ada kunci dalam database (lapisan opsi pertama) dan bahwa itu mungkin berisi nilai yang telah ditentukan (lapisan opsi kedua).

Pendekatan ini lebih baik daripada menggunakan pengecualian dan cukup mudah dipahami asalkan Optionalbukan hal baru bagi Anda.

Harap perhatikan juga bahwa Optionalada beberapa fungsi kenyamanan, sehingga Anda tidak harus melakukan semua pekerjaan sendiri. Ini termasuk:

  • static static <T> Optional<T> fromNullable(T nullableReference)untuk mengkonversi input database Anda ke Optionaltipe
  • abstract T or(T defaultValue) untuk mendapatkan nilai kunci dari lapisan opsi dalam atau (jika tidak ada) dapatkan nilai kunci default Anda
valenterri
sumber
1
Optional<Optional<String>>terlihat jahat, meskipun memiliki daya tarik yang harus saya akui
:)
Bisakah Anda jelaskan lebih lanjut mengapa itu terlihat jahat? Ini sebenarnya pola yang sama dengan List<List<String>>. Anda tentu saja dapat (jika bahasa memungkinkan) membuat jenis baru seperti DBConfigKeyyang membungkus Optional<Optional<String>>dan membuatnya lebih mudah dibaca jika Anda suka itu.
valenterri
2
@valenterry Sangat berbeda. Daftar daftar adalah cara yang sah untuk menyimpan beberapa jenis data; opsional dari opsional adalah cara yang tidak lazim untuk menunjukkan dua jenis masalah yang mungkin dimiliki data.
Ypnypn
Opsi opsi adalah seperti daftar daftar di mana setiap daftar memiliki satu atau tidak ada elemen. Ini pada dasarnya sama. Hanya karena tidak sering digunakan di Jawa tidak berarti itu buruk secara inheren.
valenterri
1
@valentri jahat dalam arti bahwa maksud Jon Skeet; jadi tidak terlalu buruk, tapi agak jahat, "kode pintar". Saya kira itu hanya sedikit barok, opsional dari opsional. Tidak jelas secara eksplisit tingkat opsional yang mewakili apa. Saya akan mengambil dua kali lipat jika saya temui dalam kode seseorang. Anda mungkin benar bahwa itu terasa aneh hanya karena itu tidak biasa, tidak otomatis. Mungkin tidak ada yang mewah untuk programmer bahasa fungsional, dengan monad mereka dan semuanya. Meskipun saya tidak akan melakukannya sendiri, saya masih memberi +1 pada jawaban Anda karena menarik
Konrad Morawski
5

Saya tahu saya terlambat ke pesta, tetapi bagaimanapun juga use case Anda menyerupai bagaimana JavaProperties memungkinkan seseorang menentukan set properti default juga, yang akan diperiksa jika tidak ada kunci terkait yang dimuat oleh instance.

Melihat bagaimana implementasi dilakukan untuk Properties.getProperty(String)(dari Java 7):

Object oval = super.get(key);
String sval = (oval instanceof String) ? (String)oval : null;
return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;

Benar-benar tidak perlu "menyalahgunakan pengecualian untuk mengontrol alur", seperti yang telah Anda kutip.

Pendekatan serupa, tetapi sedikit lebih singkat, untuk jawaban @ BЈовић juga dapat:

public String getByKey(String key) {
    if (!valuesFromDatabase.containsKey(key)) {
        valuesFromDatabase.put(key, defaultValues.get(key));
    }
    return valuesFromDatabase.get(key);
}
hjk
sumber
4

Meskipun saya pikir jawaban BЈовић baik-baik saja jika getValueByKeydiperlukan di tempat lain, saya tidak berpikir solusi Anda buruk jika program Anda mengandung kedua kasus penggunaan:

  • pengambilan dengan kunci dengan penciptaan otomatis jika kunci tidak ada sebelumnya, dan

  • pengambilan tanpa automatisme itu, tanpa mengubah apa pun dalam database, repositori, atau peta kunci (pikirkan getValueByKeybeeing public, bukan private)

Jika ini adalah situasi Anda, dan selama kinerja dapat diterima, saya pikir solusi yang Anda usulkan sepenuhnya ok. Ini memiliki keuntungan menghindari duplikasi kode pengambilan, tidak bergantung pada kerangka kerja pihak ketiga, dan itu cukup sederhana (setidaknya di mata saya).

Bahkan, dalam situasi seperti itu, tergantung pada konteksnya apakah kunci yang hilang adalah situasi "luar biasa" atau tidak. Untuk konteks di mana itu, sesuatu seperti getValueByKeydiperlukan. Untuk konteks di mana diharapkan pembuatan kunci otomatis, menyediakan metode yang menggunakan kembali fungsi yang sudah tersedia, menelan pengecualian dan memberikan perilaku kegagalan yang berbeda, sangat masuk akal. Ini dapat diartikan sebagai perpanjangan atau "dekorator" dari getValueByKey, tidak sebanyak fungsi di mana "pengecualian disalahgunakan untuk aliran kontrol".

Tentu saja, ada alternatif ketiga: buat metode pribadi ketiga yang mengembalikan Tuple<Boolean, String>, seperti yang Anda sarankan, dan gunakan kembali metode ini baik dalam getValueByKeymaupun getByKey. Untuk kasus yang lebih rumit yang mungkin memang alternatif yang lebih baik, tetapi untuk kasus sederhana seperti yang ditunjukkan di sini, ini memiliki IMHO bau rekayasa, dan saya ragu kode menjadi benar-benar lebih dapat dipertahankan dengan cara itu. Saya di sini dengan jawaban paling atas di sini oleh Karl Bielefeldt :

"Anda seharusnya tidak merasa tidak enak tentang menggunakan pengecualian ketika menyederhanakan kode Anda".

Doc Brown
sumber
3

Optionaladalah solusi yang tepat. Namun, jika Anda lebih suka, ada alternatif yang memiliki nuansa "dua nol" yang kurang, pertimbangkan penjaga.

Tentukan string statis privat keyNotFoundSentinel, dengan nilai new String(""). *

Sekarang metode pribadi bisa return keyNotFoundSentineldaripada throw new KeyNotFoundException(). Metode publik dapat memeriksa nilai itu

String rval = getValueByKey(key);
if (rval == keyNotFoundSentinel) { // reference equality, not string.equals
    ... // generate default value
} else {
    return rval;
}

Pada kenyataannya, nulladalah kasus khusus seorang penjaga. Ini adalah nilai sentinel yang ditentukan oleh bahasa, dengan beberapa perilaku khusus (yaitu perilaku yang didefinisikan dengan baik jika Anda memanggil metode aktif null). Kebetulan sangat berguna untuk memiliki seorang penjaga seperti itu yang hampir selalu menggunakan bahasa itu.

* Kami sengaja menggunakan new String("")alih-alih hanya ""untuk mencegah Java dari "interning" string, yang akan memberikan referensi yang sama dengan string kosong lainnya. Karena melakukan langkah ekstra ini, kami dijamin bahwa String yang dirujuk oleh keyNotFoundSentineladalah contoh unik, yang perlu kami pastikan tidak akan pernah muncul di peta itu sendiri.

Cort Ammon - Reinstate Monica
sumber
Ini, IMHO, adalah jawaban terbaik.
fgp
2

Belajar dari kerangka kerja yang dipelajari dari semua titik sakit di Jawa:

.NET menyediakan dua solusi yang jauh lebih elegan untuk masalah ini, dicontohkan oleh:

Yang terakhir sangat mudah untuk menulis di Jawa, yang pertama hanya membutuhkan kelas pembantu "referensi kuat".

Ben Voigt
sumber
Alternatif yang ramah kovarian untuk Dictionarypolanya adalah T TryGetValue(TKey, out bool).
supercat