Apakah selalu masuk akal untuk "memprogram ke antarmuka" di Java?

9

Saya telah melihat diskusi di pertanyaan ini mengenai bagaimana kelas yang mengimplementasikan dari antarmuka akan dipakai. Dalam kasus saya, saya sedang menulis sebuah program yang sangat kecil di Jawa yang menggunakan instance dari TreeMap, dan menurut pendapat semua orang di sana, itu harus dipakai seperti:

Map<X> map = new TreeMap<X>();

Dalam program saya, saya memanggil fungsi map.pollFirstEntry(), yang tidak dideklarasikan di Mapantarmuka (dan beberapa lainnya yang ada di Mapantarmuka juga). Saya telah berhasil melakukan ini dengan melakukan casting ke TreeMap<X>mana saja saya memanggil metode ini seperti:

someEntry = ((TreeMap<X>) map).pollFirstEntry();

Saya memahami keuntungan dari pedoman inisialisasi seperti dijelaskan di atas untuk program besar, namun untuk program yang sangat kecil di mana objek ini tidak akan diteruskan ke metode lain, saya akan berpikir itu tidak perlu. Namun, saya menulis kode sampel ini sebagai bagian dari aplikasi pekerjaan, dan saya tidak ingin kode saya terlihat buruk atau berantakan. Apa yang akan menjadi solusi paling elegan?

EDIT: Saya ingin menunjukkan bahwa saya lebih tertarik pada praktik pengkodean luas yang baik daripada penerapan fungsi tertentu TreeMap. Seperti yang telah ditunjukkan beberapa jawaban (dan saya telah menandai sebagai jawaban yang pertama melakukannya), tingkat abstraksi yang lebih tinggi mungkin harus digunakan, tanpa kehilangan fungsionalitas.

jimijazz
sumber
1
Gunakan TreeMap saat Anda membutuhkan fitur. Itu mungkin pilihan desain untuk alasan tertentu sehingga harus juga menjadi bagian dari implementasi.
@JordanReiter haruskah saya menambahkan pertanyaan baru dengan konten yang sama, atau apakah ada mekanisme lintas-posting internal?
jimijazz
2
"Saya mengerti keuntungan dari pedoman inisialisasi seperti yang dijelaskan di atas untuk program besar" Harus membuang semua tempat tidak menguntungkan apa pun ukuran program
Ben Aaronson

Jawaban:

23

"Memprogram ke antarmuka" tidak berarti "menggunakan versi yang paling abstrak yang mungkin". Dalam hal ini semua orang hanya akan menggunakan Object.

Artinya adalah bahwa Anda harus mendefinisikan program Anda terhadap abstraksi serendah mungkin tanpa kehilangan fungsionalitas . Jika Anda memerlukan TreeMapmaka Anda perlu mendefinisikan kontrak menggunakan a TreeMap.

Jeroen Vannevel
sumber
2
TreeMap bukan antarmuka, ini kelas implementasi. Ini mengimplementasikan Peta antarmuka, SortedMap, dan NavigableMap. Metode yang dijelaskan adalah bagian dari antarmuka NavigableMap . Menggunakan TreeMap akan mencegah implementasi beralih ke ConcurrentSkipListMap (misalnya) yang merupakan seluruh titik pengkodean ke antarmuka daripada implementasi.
3
@MichaelT: Saya tidak melihat abstraksi yang tepat yang dia butuhkan dalam skenario khusus ini, jadi saya gunakan TreeMapsebagai contoh. "Program ke antarmuka" tidak boleh dianggap sebagai antarmuka atau kelas abstrak - implementasi juga dapat dianggap sebagai antarmuka.
Jeroen Vannevel
1
Meskipun antarmuka publik / metode dari kelas implementasi secara teknis adalah 'antarmuka', itu memecah konsep di balik LSP dan mencegah substitusi dari subkelas yang berbeda yang mengapa Anda ingin memprogram ke public interfacedaripada 'metode publik implementasi' .
@ JoeroenVannevel Saya setuju bahwa pemrograman ke antarmuka dapat dilakukan ketika antarmuka sebenarnya diwakili oleh kelas. Namun, saya tidak melihat apa manfaatnya menggunakan TreeMaplebih SortedMapatauNavigableMap
toniedzwiedz
16

Jika Anda masih ingin menggunakan antarmuka, Anda bisa menggunakan

NavigableMap <X, Y> map = new TreeMap<X, Y>();

tidak perlu selalu menggunakan antarmuka tetapi sering ada titik di mana Anda ingin mengambil tampilan yang lebih umum yang memungkinkan Anda untuk mengganti implementasi (mungkin untuk pengujian) dan ini mudah jika semua referensi ke objek diabstraksikan sebagai jenis antarmuka.


sumber
3

Poin di balik pengkodean ke antarmuka daripada implementasi adalah untuk menghindari bocornya detail implementasi yang jika tidak akan menghambat program Anda.

Pertimbangkan situasi di mana versi asli kode Anda menggunakan HashMapdan memaparkannya.

private HashMap foo = new HashMap();
public HashMap getFoo() { return foo; }  // This is bad, don't do this.

Ini berarti bahwa setiap perubahan getFoo()adalah perubahan API yang melanggar dan akan membuat orang yang menggunakannya tidak senang. Jika semua yang Anda jamin adalah fooPeta, Anda harus mengembalikannya.

private Map foo = new HashMap();
public Map getFoo() { return foo; }

Ini memberi Anda fleksibilitas untuk mengubah cara kerja kode Anda. Anda menyadari bahwa foosebenarnya perlu menjadi Peta yang mengembalikan hal-hal dalam urutan tertentu.

private NavigableMap foo = new TreeMap();
public Map getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

Dan itu tidak merusak apa pun untuk sisa kode.

Anda nantinya dapat memperkuat kontrak yang diberikan kelas tanpa melanggar apa pun juga.

private NavigableMap foo = new TreeMap();
public NavigableMap getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

Ini menggali prinsip substitusi Liskov

Substitutability adalah prinsip dalam pemrograman berorientasi objek. Ini menyatakan bahwa, dalam program komputer, jika S adalah subtipe dari T, maka objek tipe T dapat diganti dengan objek tipe S (yaitu, objek tipe S dapat menggantikan objek tipe T) tanpa mengubah salah satu yang diinginkan properti dari program itu (kebenaran, tugas dilakukan, dll).

Karena NavigableMap adalah subtipe Peta, penggantian ini dapat dilakukan tanpa mengubah program.

Mengekspos tipe implementasi menyulitkan untuk mengubah cara kerja program secara internal ketika perubahan perlu dilakukan. Ini adalah proses yang menyakitkan dan berkali-kali menciptakan solusi buruk yang hanya berfungsi untuk menimbulkan lebih banyak rasa sakit pada pembuat kode nanti (Saya melihat Anda pembuat kode sebelumnya yang terus menyeret data antara LinkedHashMap dan TreeMap karena suatu alasan - percayalah, setiap kali saya lihat namamu di svn menyalahkan aku khawatir).

Anda masih ingin menghindari bocornya jenis implementasi. Sebagai contoh, Anda mungkin ingin mengimplementasikan ConcurrentSkipListMap sebagai gantinya karena beberapa karakteristik kinerja atau Anda hanya suka java.util.concurrent.ConcurrentSkipListMapdaripada java.util.TreeMapdalam pernyataan impor atau apa pun.


sumber
1

Setuju dengan jawaban lain bahwa Anda harus menggunakan kelas (atau antarmuka) paling umum yang sebenarnya Anda perlukan sesuatu, dalam hal ini TreeMap (atau seperti seseorang yang disarankan NavigableMap). Tapi saya ingin menambahkan bahwa ini pastinya lebih baik daripada casting di mana-mana, yang akan menjadi bau yang jauh lebih besar dalam hal apapun. Lihat /programming/4167304/why-should-casting-be-avoided karena beberapa alasan.

Sebastiaan van den Broek
sumber
1

Ini tentang mengomunikasikan maksud Anda tentang bagaimana objek harus digunakan. Misalnya, jika metode Anda mengharapkan Mapobjek dengan urutan iterasi yang dapat diprediksi :

private Map<String, String> processOrderedMap(LinkedHashMap<String, String> input) {
    // ...
}

Kemudian, jika Anda benar-benar perlu memberi tahu penelepon tentang metode di atas bahwa ia juga mengembalikan Mapobjek dengan urutan iterasi yang dapat diprediksi , karena ada harapan seperti itu untuk beberapa alasan:

private LinkedHashMap<String,String> processOrderedMap(LinkedHashMap<String,String> input) {
    // ...
}

Tentu saja, penelepon masih dapat memperlakukan objek kembali Mapseperti itu, tetapi itu di luar ruang lingkup metode Anda:

private Map<String, String> output = processOrderedMap(input);

Mengambil langkah mundur

Saran umum pengkodean ke antarmuka (umumnya) dapat diterapkan, karena biasanya antarmuka yang memberikan jaminan apa yang harus dapat dilakukan objek, alias kontrak . Banyak pemula mulai dengan HashMap<K, V> map = new HashMap<>()dan mereka disarankan untuk menyatakan bahwa sebagai Map, karena HashMaptidak menawarkan apa pun lebih dari apa yang Mapseharusnya dilakukan. Dari ini, mereka kemudian akan dapat memahami (semoga) mengapa metode mereka harus mengambil Mapalih - alih a HashMap, dan ini memungkinkan mereka menyadari fitur warisan di OOP.

Mengutip hanya satu baris dari entri Wikipedia prinsip favorit semua orang yang terkait dengan topik ini:

Ini adalah hubungan semantik daripada hanya sintaksis karena bermaksud untuk menjamin interoperabilitas semantik dari jenis dalam hierarki ...

Dengan kata lain, menggunakan Mapdeklarasi bukan karena masuk akal 'secara sintaksis', melainkan doa pada objek hanya harus peduli bahwa itu adalah jenis Map.

Kode pembersih

Saya menemukan bahwa ini memungkinkan saya untuk menulis kode pembersih juga, terutama ketika datang ke unit testing. Membuat HashMapdengan entri uji baca-saja tunggal membutuhkan lebih dari satu baris (tidak termasuk penggunaan inisialisasi penjepit ganda), ketika saya dapat dengan mudah menggantinya dengan Collections.singletonMap().

hjk
sumber