Praktek yang baik atau buruk untuk menutupi koleksi Java dengan nama kelas yang bermakna?

46

Akhir-akhir ini saya terbiasa "menutupi" koleksi Java dengan nama kelas yang ramah manusia. Beberapa contoh sederhana:

// Facade class that makes code more readable and understandable.
public class WidgetCache extends Map<String, Widget> {
}

Atau:

// If you saw a ArrayList<ArrayList<?>> being passed around in the code, would you
// run away screaming, or would you actually understand what it is and what
// it represents?
public class Changelist extends ArrayList<ArrayList<SomePOJO>> {
}

Seorang kolega menunjukkan kepada saya bahwa ini adalah praktik yang buruk, dan memperkenalkan jeda / latensi, serta menjadi anti-pola OO. Saya dapat memahaminya memperkenalkan tingkat kinerja yang sangat kecil , tetapi tidak dapat membayangkan itu sama sekali signifikan. Jadi saya bertanya: apakah ini baik atau buruk untuk dilakukan, dan mengapa?

herpylderp
sumber
11
Jauh lebih sederhana dari ini. Ini praktik buruk karena saya membayangkan Anda memperluas implementasi Java JDK Collection dasar itu. Di Java, Anda hanya dapat memperluas satu kelas, jadi Anda harus berpikir dan mendesain lebih banyak ketika Anda memiliki ekstensi. Di Jawa, gunakan extended sparingly.
InformedA
ChangeListkompilasi akan rusak pada extends, karena Daftar adalah antarmuka, memerlukan implements. @randomA apa yang Anda bayangkan tidak tepat karena kesalahan ini
agas
@gnat Tidak ada salahnya, saya berasumsi bahwa dia sedang memperpanjang implementationyaitu HashMapatau TreeMapdan apa yang dia punya ada kesalahan ketik.
InformedA
4
Ini adalah praktik yang BURUK. BURUK BURUK BURUK. Jangan lakukan ini. Semua orang tahu apa itu Peta <String, Widget>. Tapi WidgetCache? Sekarang saya perlu membuka WidgetCache.java, saya harus ingat bahwa WidgetCache hanyalah sebuah peta. Saya harus memeriksa setiap kali saya versi baru keluar bahwa Anda belum menambahkan sesuatu yang baru ke WidgetCache. Tuhan tidak, jangan pernah lakukan ini.
Miles Rout
"Jika kamu melihat ArrayList <ArrayList <? >> sedang diedarkan dalam kode, apakah kamu akan lari sambil berteriak ...?" Tidak, saya cukup nyaman dengan koleksi generik bersarang. Dan kamu juga harus begitu.
Kevin Krumwiede

Jawaban:

75

Lag / Latensi? Saya memanggil BS tentang itu. Seharusnya tidak ada overhead dari praktik ini. ( Sunting: Telah ditunjukkan dalam komentar bahwa ini dapat, pada kenyataannya, menghambat optimasi yang dilakukan oleh HotSpot VM. Saya tidak cukup tahu tentang implementasi VM untuk mengkonfirmasi atau menyangkal ini. Saya mendasarkan komentar saya pada C ++ implementasi fungsi virtual.)

Ada beberapa kode overhead. Anda harus membuat semua konstruktor dari kelas dasar yang Anda inginkan, meneruskan parameternya.

Saya juga tidak melihatnya sebagai anti-pola, per se. Namun, saya melihatnya sebagai peluang yang terlewatkan. Alih-alih membuat kelas yang mendapatkan kelas dasar hanya untuk penggantian nama, bagaimana kalau Anda membuat kelas yang berisi koleksi dan menawarkan antarmuka yang ditingkatkan khusus untuk kasus tertentu? Haruskah cache widget Anda benar-benar menawarkan antarmuka penuh peta? Atau haruskah ia menawarkan antarmuka khusus?

Lebih jauh, dalam hal koleksi, polanya sama sekali tidak bekerja bersama dengan aturan umum penggunaan antarmuka, bukan implementasi - yaitu, dalam kode koleksi polos, Anda akan membuat HashMap<String, Widget>, dan kemudian menetapkannya ke variabel tipe Map<String, Widget>. Anda WidgetCachetidak dapat memperluas Map<String, Widget>, karena itu adalah antarmuka. Itu tidak bisa berupa antarmuka yang memperluas antarmuka dasar, karena HashMap<String, Widget>tidak mengimplementasikan antarmuka itu, dan begitu pula koleksi standar lainnya. Dan sementara Anda bisa menjadikannya kelas yang memanjang HashMap<String, Widget>, Anda kemudian harus mendeklarasikan variabel sebagai WidgetCacheatau Map<String, Widget>, dan yang pertama kehilangan Anda fleksibilitas untuk mengganti koleksi yang berbeda (mungkin beberapa koleksi pemuatan malas ORM), sedangkan jenis kedua mengalahkan poin memiliki kelas.

Beberapa tandingan ini juga berlaku untuk kelas khusus yang saya usulkan.

Ini semua poin yang perlu dipertimbangkan. Ini mungkin atau mungkin bukan pilihan yang tepat. Dalam kedua kasus tersebut, argumen yang ditawarkan kolega Anda tidak valid. Jika dia pikir itu anti-pola, dia harus menyebutkannya.

Sebastian Redl
sumber
1
Perhatikan bahwa walaupun sangat mungkin untuk memiliki dampak kinerja, itu tidak akan menjadi mengerikan (kehilangan beberapa optimasi inlining dan mendapatkan panggilan tambahan dalam kasus terburuk - tidak bagus di loop terdalam Anda tetapi sebaliknya mungkin baik-baik saja).
Voo
5
Jawaban yang sangat bagus ini memberi tahu semua orang "jangan menilai keputusan dengan overhead kinerja" - dan satu-satunya diskusi di sini di bawah ini adalah "bagaimana HotSpot menangani ini, apakah ada beberapa (dalam 99,9% dari semua kasus) dampak kinerja yang tidak relevan" - teman, apakah Anda bahkan repot membaca lebih dari kalimat pertama?
Doc Brown
16
+1 untuk "Alih-alih membuat kelas yang mendapatkan kelas dasar hanya untuk penggantian nama, bagaimana kalau Anda malah membuat kelas yang berisi koleksi dan menawarkan antarmuka yang ditingkatkan dengan kasus khusus?"
user11153
6
Contoh praktis yang mengilustrasikan poin utama dalam jawaban ini: Dokumentasi untuk public class Properties extends Hashtable<Object,Object>dalam java.utilmengatakan "Karena Properties mewarisi dari Hashtable, metode put dan putAll dapat diterapkan ke objek Properties. Penggunaannya sangat tidak dianjurkan karena memungkinkan penelepon untuk memasukkan entri yang kuncinya atau nilai bukan String. " Komposisi akan jauh lebih bersih.
Patricia Shanahan
3
@DocBrown Tidak, jawaban ini mengklaim bahwa tidak ada dampak kinerja sama sekali. Jika Anda membuat pernyataan yang kuat, Anda bisa mendukungnya, jika tidak, orang mungkin akan memanggil Anda untuk membantunya. Inti dari komentar (pada jawaban) adalah untuk menunjukkan fakta atau asumsi yang tidak akurat atau menambahkan catatan yang bermanfaat, tetapi tentu saja tidak untuk memberi selamat kepada orang, itulah gunanya sistem pemungutan suara.
Voo
25

Menurut IBM ini sebenarnya anti-pola. Kelas seperti 'typedef' ini disebut tipe psuedo.

Artikel ini menjelaskannya jauh lebih baik daripada saya, tetapi saya akan mencoba meringkasnya jika tautannya turun:

  • Kode apa pun yang mengharapkan WidgetCachetidak dapat menangani aMap<String, Widget>
  • Pseudotip ini 'viral' ketika menggunakan beberapa paket yang menyebabkan ketidakcocokan sementara tipe dasar (hanya Peta konyol <...>) akan bekerja dalam semua kasus di semua paket.
  • Tipe pseudo seringkali konkret, mereka tidak mengimplementasikan antarmuka spesifik karena kelas dasarnya hanya mengimplementasikan versi generik.

Dalam artikel itu mereka mengusulkan trik berikut untuk membuat hidup lebih mudah tanpa menggunakan tipe pseudo:

public static <K,V> Map<K,V> newHashMap() {
    return new HashMap<K,V>(); 
}

Map<Socket, Future<String>> socketOwner = Util.newHashMap();

Yang berfungsi karena inferensi tipe otomatis.

(Saya sampai pada jawaban ini melalui pertanyaan stack overflow terkait)

Roy T.
sumber
13
Bukankah kebutuhan untuk newHashMapdiselesaikan sekarang oleh operator berlian?
svick
Benar sekali, lupakan itu. Saya biasanya tidak bekerja di Jawa.
Roy T.
Saya berharap bahasa akan memungkinkan untuk semesta jenis variabel yang memegang referensi untuk contoh jenis lain, tetapi masih bisa mendukung pemeriksaan waktu kompilasi, sehingga nilai jenis WidgetCachedapat ditugaskan ke variabel jenis WidgetCacheatau Map<String,Widget>tanpa gips, tetapi ada adalah sarana dimana metode statis di WidgetCachedapat membuat mengambil referensi ke Map<String,Widget>dan mengembalikannya sebagai tipe WidgetCachesetelah melakukan validasi yang diinginkan. Fitur seperti itu bisa sangat berguna dengan obat-obatan generik, yang tidak perlu dihapus jenisnya (karena ...
supercat
... jenisnya hanya akan ada dalam pikiran penyusun). Untuk banyak jenis yang bisa berubah, ada dua jenis bidang referensi yang umum: satu yang memegang satu-satunya referensi ke sebuah instance yang dapat dimutasi, atau satu yang memiliki referensi yang dapat dibagi dengan instance yang tidak ada yang diizinkan untuk bermutasi. Akan sangat membantu untuk dapat memberikan nama yang berbeda untuk kedua jenis bidang tersebut, karena mereka memerlukan pola penggunaan yang berbeda, tetapi keduanya harus memiliki referensi untuk jenis instance objek yang sama.
supercat
5
Ini adalah argumen yang bagus untuk alasan mengapa Java perlu mengetik alias.
GlenPeterson
13

Hit kinerja akan dibatasi paling banyak pada pencarian vtable, yang kemungkinan besar sudah Anda lakukan. Itu bukan alasan yang sah untuk menentangnya.

Situasinya cukup umum sehingga sebagian besar semua bahasa pemrograman yang diketik secara statis memiliki sintaks khusus untuk jenis alias, biasanya disebut a typedef. Java mungkin tidak menyalinnya karena pada awalnya tidak memiliki tipe parameter. Memperluas kelas bukanlah hal yang ideal, karena alasan Sebastian begitu baik dalam menjawabnya, tetapi ini bisa menjadi solusi yang masuk akal untuk sintaksis terbatas Java.

Typedefs memiliki sejumlah keunggulan. Mereka mengekspresikan maksud programmer lebih jelas, dengan nama yang lebih baik, pada tingkat abstraksi yang lebih tepat. Mereka lebih mudah untuk mencari tujuan debugging atau refactoring. Pertimbangkan menemukan di mana-mana a WidgetCachedigunakan versus menemukan penggunaan spesifik a Map. Mereka lebih mudah untuk diubah, misalnya jika nanti Anda menemukan yang Anda butuhkan LinkedHashMap, atau bahkan wadah kustom Anda sendiri.

Karl Bielefeldt
sumber
Ada juga overhead yang sangat kecil di konstruktor.
Stephen C
11

Saya menyarankan Anda, seperti yang telah disebutkan orang lain, untuk menggunakan komposisi di atas warisan, sehingga Anda hanya dapat mengekspos metode yang benar-benar diperlukan, dengan nama yang cocok dengan kasus penggunaan yang dimaksudkan. Apakah pengguna kelas Anda benar-benar perlu tahu bahwa itu WidgetCacheadalah peta? Dan dapat melakukan apa pun yang mereka inginkan? Atau mereka hanya perlu tahu bahwa itu adalah cache untuk widget?

Contoh kelas dari basis kode saya, dengan solusi untuk masalah serupa yang Anda deskripsikan:

public class Translations {

    private Map<Locale, Properties> translations = new HashMap<>();

    public void appendMessage(Locale locale, String code, String message) {
        /* code */
    }

    public void addMessages(Locale locale, Properties messages) {
        /* code */
    }

    public String getMessage(Locale locale, String code) {
        /* code */
    }

    public boolean localeExists(Locale locale) {
        /* code */
    }
}

Anda dapat melihat bahwa secara internal itu "hanya peta", tetapi antarmuka publik tidak menunjukkan ini. Dan ia memiliki metode "ramah-programmer" seperti appendMessage(Locale locale, String code, String message)cara memasukkan entri baru yang lebih mudah dan lebih bermakna. Dan pengguna kelas tidak dapat melakukan, misalnya translations.clear(), karena Translationstidak memperluas Map.

Secara opsional, Anda selalu dapat mendelegasikan beberapa metode yang diperlukan ke peta yang digunakan secara internal.

pengguna11153
sumber
6

Saya melihat ini sebagai contoh dari abstraksi yang bermakna. Abstraksi yang baik memiliki beberapa sifat:

  1. Itu menyembunyikan detail implementasi yang tidak relevan dengan kode yang memakannya.

  2. Itu hanya serumit yang seharusnya.

Dengan memperluas, Anda mengekspos seluruh antarmuka induk, tetapi dalam banyak kasus banyak yang mungkin lebih baik disembunyikan, jadi Anda ingin melakukan apa yang disarankan Sebastian Redl dan mendukung komposisi daripada pewarisan dan menambahkan turunan dari induk sebagai anggota pribadi kelas khusus Anda. Metode antarmuka apa pun yang masuk akal untuk abstraksi Anda dapat dengan mudah didelegasikan ke (dalam kasus Anda) koleksi dalam.

Adapun dampak kinerja, itu selalu ide yang baik untuk mengoptimalkan kode untuk keterbacaan pertama, dan jika dampak kinerja diduga, profil kode untuk membandingkan kedua implementasi.

Mike Partridge
sumber
4

Beri +1 ke jawaban lain di sini. Saya juga akan menambahkan bahwa itu sebenarnya dianggap praktik yang sangat baik oleh komunitas Desain Domain Driven (DDD). Mereka menganjurkan bahwa domain Anda dan interaksi dengannya harus memiliki makna domain semantik yang bertentangan dengan struktur data yang mendasarinya. A Map<String, Widget>bisa berupa cache, tetapi bisa juga berupa sesuatu yang lain, apa yang telah Anda lakukan dengan benar dalam My Not So Humble Opinion (IMNSHO) adalah memodelkan apa yang ditampilkan oleh koleksi, dalam hal ini cache.

Saya akan menambahkan suntingan penting karena pembungkus kelas domain di sekitar struktur data yang mendasarinya mungkin juga harus memiliki variabel anggota lain atau fungsi yang benar-benar menjadikannya kelas domain dengan interaksi yang berlawanan dengan hanya struktur data (jika saja Jawa memiliki Jenis Nilai) , kami akan mendapatkannya di Jawa 10 - janji!)

Ini akan menarik untuk melihat apa dampak aliran Java 8 akan memiliki pada semua ini, saya bisa membayangkan bahwa mungkin beberapa antarmuka publik akan lebih suka berurusan dengan Stream (masukkan common Java primitive atau String) sebagai lawan dari objek Java.

Martijn Verburg
sumber