Saya baru saja berdiskusi tentang pilihan desain setelah ulasan kode. Saya ingin tahu apa pendapat Anda.
Ada Preferences
kelas 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 null
akan ambigu, karena null
merupakan nilai legal sempurna, jadi tidak ada yang tahu apakah itu berarti bahwa kuncinya tidak ada di sana, atau jika ada null
.
getValueByKey
harus mengembalikan semacam a Tuple<Boolean, String>
, bool diatur ke true jika kuncinya sudah ada, sehingga kita dapat membedakan antara (true, null)
dan (false, null)
. ( out
Parameter 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 Optional
di tempat pertama.
Pilihan lain adalah dengan memeriksa secara eksplisit apakah kunci tersebut ada (tambahkan boolean containsKey(String key)
metode dan panggil getValueByKey
saja jika kami telah menyatakan bahwa kunci itu ada).
Akhirnya, kita juga bisa sebaris metode pribadi, tetapi sebenarnya getByKey
agak 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?
sumber
getValueByKey
juga publik.Jawaban:
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):Pengecualian harus dibuang hanya jika Anda tidak tahu cara menyelesaikan kesalahan secara lokal.
sumber
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".
sumber
Optional
dapat menyimpan null.Optional.of()
hanya mengambil referensi bukan-nol danOptional.fromNullable()
memperlakukan nol sebagai "nilai tidak ada".Optional.absent()
siap membantu Anda. Jadi,Optional.fromNullable(string)
akan sama denganOptional.<String>absent()
jikastring
null, atauOptional.of(string)
jika tidak.Optional.absent()
mencakup salah satu skenario ini. Bagaimana Anda mewakili yang lain?null
.null
!" daripada menyatakannya sebagai kembaliOptional<...>
. . .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.)
Optional
Solusinya sangat cocok untuk masalah tersebut. Mungkin agak ironis untuk menyimpannull
dalamOptional
, 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
Optional
mengizinkan penyimpanannull
. 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 petaMap<String, Optional<String>>
, tetapi penanganannyaOptional<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.
sumber
Map<String, String>
pada tingkat tabel database, karenavalues
kolom 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).(false, "foo")
seharusnya berarti?Map<String, Optional<String>>
dan kemudian Anda akan berakhir denganOptional<Optional<String>>
. Solusi yang kurang membingungkan adalah dengan menggulung Opsional Anda sendiri yang memungkinkannull
, atau tipe data aljabar serupa yang melarangnull
tetapi 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.Masalahnya persis seperti ini. Tapi Anda sudah memposting solusinya:
Namun, jangan gunakan
null
atauOptional
. Anda hanya dapat dan harus menggunakannyaOptional
. Untuk perasaan "retas" Anda, gunakan saja itu bersarang, sehingga Anda berakhir denganOptional<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
Optional
bukan hal baru bagi Anda.Harap perhatikan juga bahwa
Optional
ada 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 keOptional
tipeabstract T or(T defaultValue)
untuk mendapatkan nilai kunci dari lapisan opsi dalam atau (jika tidak ada) dapatkan nilai kunci default Andasumber
Optional<Optional<String>>
terlihat jahat, meskipun memiliki daya tarik yang harus saya akuiList<List<String>>
. Anda tentu saja dapat (jika bahasa memungkinkan) membuat jenis baru sepertiDBConfigKey
yang membungkusOptional<Optional<String>>
dan membuatnya lebih mudah dibaca jika Anda suka itu.Saya tahu saya terlambat ke pesta, tetapi bagaimanapun juga use case Anda menyerupai bagaimana Java
Properties
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):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:
sumber
Meskipun saya pikir jawaban BЈовић baik-baik saja jika
getValueByKey
diperlukan 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
getValueByKey
beeing 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
getValueByKey
diperlukan. 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" darigetValueByKey
, 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 dalamgetValueByKey
maupungetByKey
. 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 :sumber
Optional
adalah 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 nilainew String("")
. *Sekarang metode pribadi bisa
return keyNotFoundSentinel
daripadathrow new KeyNotFoundException()
. Metode publik dapat memeriksa nilai ituPada kenyataannya,
null
adalah 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 aktifnull
). 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 olehkeyNotFoundSentinel
adalah contoh unik, yang perlu kami pastikan tidak akan pernah muncul di peta itu sendiri.sumber
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:
Dictionary<TKey, TValue>.TryGetValue(TKey, out TValue)
Nullable<T>.GetValueOrDefault(T default)
Yang terakhir sangat mudah untuk menulis di Jawa, yang pertama hanya membutuhkan kelas pembantu "referensi kuat".
sumber
Dictionary
polanya adalahT TryGetValue(TKey, out bool)
.