Ada banyak hal yang dapat dikatakan tentang budaya Jawa, tetapi saya pikir dalam kasus yang Anda hadapi sekarang, ada beberapa aspek penting:
- Kode perpustakaan ditulis sekali tetapi digunakan lebih sering. Meskipun bagus untuk meminimalkan biaya penulisan perpustakaan, mungkin dalam jangka panjang akan lebih bermanfaat untuk menulis dengan cara yang meminimalkan biaya tambahan untuk menggunakan perpustakaan.
- Itu berarti tipe self-documenting hebat: nama metode membantu memperjelas apa yang terjadi dan apa yang Anda dapatkan dari suatu objek.
- Mengetik statis adalah alat yang sangat berguna untuk menghilangkan kelas kesalahan tertentu. Ini tentu saja tidak memperbaiki semuanya (orang-orang suka bercanda tentang Haskell bahwa begitu Anda mendapatkan sistem tipe untuk menerima kode Anda, itu mungkin benar), tetapi membuatnya sangat mudah untuk membuat beberapa jenis kesalahan yang salah menjadi mustahil.
- Menulis kode perpustakaan adalah tentang menentukan kontrak. Menentukan antarmuka untuk argumen dan jenis hasil Anda membuat batas-batas kontrak Anda lebih jelas. Jika sesuatu menerima atau menghasilkan tuple, tidak ada yang mengatakan apakah itu jenis tuple yang harus Anda terima atau hasilkan, dan ada sangat sedikit kendala dalam hal jenis generik (apakah ia memiliki jumlah elemen yang tepat? mereka dari tipe yang Anda harapkan?).
Kelas "Struct" dengan bidang
Seperti jawaban lain yang disebutkan, Anda bisa menggunakan kelas dengan bidang publik. Jika Anda membuat ini final, maka Anda mendapatkan kelas abadi, dan Anda akan menginisialisasi mereka dengan konstruktor:
class ParseResult0 {
public final long millis;
public final boolean isSeconds;
public final boolean isLessThanOneMilli;
public ParseResult0(long millis, boolean isSeconds, boolean isLessThanOneMilli) {
this.millis = millis;
this.isSeconds = isSeconds;
this.isLessThanOneMilli = isLessThanOneMilli;
}
}
Tentu saja, ini berarti Anda terikat pada kelas tertentu, dan apa pun yang pernah perlu untuk menghasilkan atau mengonsumsi hasil parse harus menggunakan kelas ini. Untuk beberapa aplikasi, itu tidak masalah. Bagi yang lain, itu bisa menyebabkan rasa sakit. Banyak kode Java tentang mendefinisikan kontrak, dan itu biasanya akan membawa Anda ke antarmuka.
Perangkap lain adalah bahwa dengan pendekatan berbasis kelas, Anda mengekspos bidang dan semua bidang itu harus memiliki nilai. Misalnya, isSeconds dan millis selalu harus memiliki beberapa nilai, bahkan jika isLessThanOneMilli benar. Apa yang seharusnya penafsiran nilai bidang milis ketika isLessThanOneMilli benar?
"Struktur" sebagai Antarmuka
Dengan metode statis yang diizinkan dalam antarmuka, sebenarnya relatif mudah untuk membuat tipe yang tidak dapat diubah tanpa banyak overhead sintaksis. Misalnya, saya mungkin menerapkan jenis struktur hasil yang Anda bicarakan sebagai sesuatu seperti ini:
interface ParseResult {
long getMillis();
boolean isSeconds();
boolean isLessThanOneMilli();
static ParseResult from(long millis, boolean isSeconds, boolean isLessThanOneMill) {
return new ParseResult() {
@Override
public boolean isSeconds() {
return isSeconds;
}
@Override
public boolean isLessThanOneMilli() {
return isLessThanOneMill;
}
@Override
public long getMillis() {
return millis;
}
};
}
}
Itu masih banyak boilerplate, saya benar-benar setuju, tetapi ada beberapa manfaat juga, dan saya pikir itu mulai menjawab beberapa pertanyaan utama Anda.
Dengan struktur seperti hasil parse ini, kontrak parser Anda didefinisikan dengan sangat jelas. Dalam Python, satu tuple tidak benar-benar berbeda dari tuple lain. Di Java, pengetikan statis tersedia, jadi kami sudah mengesampingkan kelas kesalahan tertentu. Misalnya, jika Anda mengembalikan tupel dengan Python, dan Anda ingin mengembalikan tupel (milid, isSeconds, isLessThanOneMilli), Anda dapat melakukan:
return (true, 500, false)
ketika Anda maksud:
return (500, true, false)
Dengan antarmuka Java semacam ini, Anda tidak dapat mengompilasi:
return ParseResult.from(true, 500, false);
sama sekali. Kamu harus melakukan:
return ParseResult.from(500, true, false);
Itulah manfaat dari bahasa yang diketik secara statis pada umumnya.
Pendekatan ini juga mulai memberi Anda kemampuan untuk membatasi nilai apa yang bisa Anda dapatkan. Misalnya, ketika memanggil getMillis (), Anda dapat memeriksa apakah isLessThanOneMilli () benar, dan jika ya, lempar IllegalStateException (misalnya), karena tidak ada nilai milis yang berarti dalam kasus itu.
Menyulitkan Melakukan Hal yang Salah
Dalam contoh antarmuka di atas, Anda masih memiliki masalah yang Anda dapat menukar argumen isSeconds dan isLessThanOneMilli secara tidak sengaja, karena keduanya memiliki tipe yang sama.
Dalam praktiknya, Anda mungkin ingin memanfaatkan TimeUnit dan durasi, sehingga Anda akan mendapatkan hasil seperti:
interface Duration {
TimeUnit getTimeUnit();
long getDuration();
static Duration from(TimeUnit unit, long duration) {
return new Duration() {
@Override
public TimeUnit getTimeUnit() {
return unit;
}
@Override
public long getDuration() {
return duration;
}
};
}
}
interface ParseResult2 {
boolean isLessThanOneMilli();
Duration getDuration();
static ParseResult2 from(TimeUnit unit, long duration) {
Duration d = Duration.from(unit, duration);
return new ParseResult2() {
@Override
public boolean isLessThanOneMilli() {
return false;
}
@Override
public Duration getDuration() {
return d;
}
};
}
static ParseResult2 lessThanOneMilli() {
return new ParseResult2() {
@Override
public boolean isLessThanOneMilli() {
return true;
}
@Override
public Duration getDuration() {
throw new IllegalStateException();
}
};
}
}
Itu menjadi lebih banyak kode, tetapi Anda hanya perlu menulisnya sekali, dan (dengan asumsi Anda mendokumentasikan hal-hal dengan benar), orang-orang yang akhirnya menggunakan kode Anda tidak harus menebak apa artinya hasilnya, dan tidak dapat secara tidak sengaja melakukan hal-hal seperti result[0]
ketika itu berarti result[1]
. Anda masih dapat membuat instance dengan cukup ringkas, dan mendapatkan data dari situ juga tidak terlalu sulit:
ParseResult2 x = ParseResult2.from(TimeUnit.MILLISECONDS, 32);
ParseResult2 y = ParseResult2.lessThanOneMilli();
Perhatikan bahwa Anda benar-benar dapat melakukan sesuatu seperti ini dengan pendekatan berbasis kelas juga. Cukup tentukan konstruktor untuk kasus yang berbeda. Anda masih memiliki masalah apa yang harus diinisialisasi dengan bidang lain, dan Anda tidak dapat mencegah akses ke sana.
Jawaban lain menyebutkan bahwa sifat Java tipe perusahaan berarti bahwa sebagian besar waktu, Anda membuat perpustakaan lain yang sudah ada, atau menulis perpustakaan untuk digunakan orang lain. API publik Anda seharusnya tidak memerlukan banyak waktu berkonsultasi dengan dokumentasi untuk menguraikan jenis hasil jika dapat dihindari.
Anda hanya menulis struktur ini satu kali, tetapi Anda membuatnya berkali-kali, jadi Anda tetap menginginkan penciptaan yang ringkas (yang Anda dapatkan). Pengetikan statis memastikan bahwa data yang Anda peroleh darinya adalah yang Anda harapkan.
Sekarang, semua yang dikatakan, masih ada tempat-tempat di mana tuple atau daftar sederhana bisa masuk akal. Mungkin ada lebih sedikit overhead dalam mengembalikan array sesuatu, dan jika itu masalahnya (dan overhead itu signifikan, yang akan Anda tentukan dengan profiling), maka menggunakan array nilai sederhana secara internal mungkin membuat banyak akal. API publik Anda mungkin masih memiliki jenis yang jelas.