Dalam proyek saya baru-baru ini, saya mendefinisikan kelas dengan header berikut:
public class Node extends ArrayList<Node> {
...
}
Namun, setelah berdiskusi dengan profesor CS saya, dia menyatakan bahwa kelas akan "mengerikan bagi ingatan" dan "praktik buruk". Saya belum menemukan yang pertama benar, dan yang kedua subjektif.
Alasan saya untuk penggunaan ini adalah bahwa saya punya ide untuk objek yang perlu didefinisikan sebagai sesuatu yang bisa memiliki kedalaman sewenang-wenang, di mana perilaku instance dapat didefinisikan baik oleh implementasi kustom atau oleh perilaku beberapa objek seperti berinteraksi . Ini akan memungkinkan untuk abstraksi objek yang implementasi fisiknya akan terdiri dari banyak sub-komponen yang berinteraksi.¹
Di sisi lain, saya melihat bagaimana ini bisa menjadi praktik yang buruk. Gagasan mendefinisikan sesuatu sebagai daftar itu sendiri tidak sederhana atau dapat diimplementasikan secara fisik.
Apakah ada alasan yang sah mengapa saya tidak boleh menggunakan ini dalam kode saya, mempertimbangkan penggunaan saya untuk itu?
¹ Jika saya perlu menjelaskan ini lebih lanjut, saya akan senang untuk; Saya hanya berusaha agar pertanyaan ini singkat.
sumber
ArrayList<Node>
, dibandingkan dengan mengimplementasikanList<Node>
?Jawaban:
Terus terang, saya tidak melihat perlunya warisan di sini. Itu tidak masuk akal;
Node
adalahArrayList
dariNode
?Jika ini hanya struktur data rekursif, Anda cukup menulis sesuatu seperti:
Yang tidak masuk akal; node memiliki daftar anak-anak atau node turunan.
sumber
Item
, danItem
beroperasi secara independen dari daftar.Node
adalahArrayList
dariNode
sebagaiNode
mengandung 0 ke banyakNode
objek. Fungsinya sama, pada dasarnya, tetapi alih-alih menjadi daftar objek sejenis, ia memiliki daftar objek sejenis. Desain asli untuk kelas yang ditulis adalah keyakinan bahwa apa punNode
dapat diekspresikan sebagai satu set subNode
, yang dilakukan oleh kedua implementasi, tetapi yang satu ini tentunya kurang membingungkan. +1Children
hanya sekali atau dua kali, dan biasanya lebih baik untuk menuliskannya dalam kasus seperti itu demi kejelasan kode.Argumen "mengerikan untuk ingatan" sepenuhnya salah, tetapi ini merupakan "praktik buruk" secara objektif . Ketika Anda mewarisi dari kelas, Anda tidak hanya mewarisi bidang dan metode yang Anda minati. Sebaliknya, Anda mewarisi segalanya . Setiap metode yang dinyatakannya, meskipun tidak berguna bagi Anda. Dan yang paling penting, Anda juga mewarisi semua kontrak dan jaminan yang disediakan kelas tersebut.
SOLID singkatan menyediakan beberapa heuristik untuk desain berorientasi objek yang baik. Di sini, saya nterface Pemisahan Prinsip (ISP) dan L iskov Pergantian Pricinple (LSP) memiliki sesuatu untuk dikatakan.
ISP memberi tahu kami untuk menjaga antarmuka kami sekecil mungkin. Tetapi dengan mewarisi dari
ArrayList
, Anda mendapatkan banyak, banyak metode. Apakah itu berarti untukget()
,remove()
,set()
(ganti), atauadd()
(insert) node anak pada indeks tertentu? Apakah masuk akal untukensureCapacity()
daftar yang mendasarinya? Apa artinyasort()
sebuah Node? Apakah pengguna kelas Anda benar-benar seharusnya mendapatkansubList()
? Karena Anda tidak dapat menyembunyikan metode yang tidak Anda inginkan, satu-satunya solusi adalah menjadikan ArrayList sebagai variabel anggota, dan meneruskan semua metode yang sebenarnya Anda inginkan:Jika Anda benar-benar menginginkan semua metode yang Anda lihat dalam dokumentasi, kami dapat beralih ke LSP. LSP memberi tahu kita bahwa kita harus dapat menggunakan subclass di mana pun kelas induknya diharapkan. Jika suatu fungsi mengambil
ArrayList
parameter sebagai dan kami meneruskannyaNode
, tidak ada yang seharusnya berubah.Kompatibilitas subclass dimulai dengan hal-hal sederhana seperti jenis tanda tangan. Saat Anda mengganti metode, Anda tidak bisa membuat tipe parameter lebih ketat karena itu mungkin mengecualikan penggunaan yang sah dengan kelas induk. Tapi itu adalah sesuatu yang diperiksa kompiler untuk kita di Jawa.
Tetapi LSP berjalan jauh lebih dalam: Kami harus menjaga kompatibilitas dengan semua yang dijanjikan oleh dokumentasi semua kelas dan antarmuka induk. Dalam jawaban mereka , Lynn telah menemukan satu kasus di mana
List
antarmuka (yang telah Anda warisi melaluiArrayList
) menjamin caraequals()
danhashCode()
metode yang seharusnya bekerja. UntukhashCode()
Anda bahkan diberikan algoritma tertentu yang harus diimplementasikan dengan tepat. Anggap Anda telah menulis iniNode
:Ini mensyaratkan bahwa
value
tidak dapat berkontribusi padahashCode()
dan tidak dapat mempengaruhiequals()
. TheList
antarmuka - yang Anda berjanji untuk menghormati dengan mewarisi dari itu - membutuhkannew Node(0).equals(new Node(123))
untuk menjadi kenyataan.Karena mewarisi dari kelas membuatnya terlalu mudah untuk secara tidak sengaja mengingkari janji yang dibuat oleh kelas induk, dan karena biasanya mengekspos lebih banyak metode daripada yang Anda maksud, umumnya disarankan agar Anda lebih memilih komposisi daripada warisan . Jika Anda harus mewarisi sesuatu, disarankan untuk hanya mewarisi antarmuka. Jika Anda ingin menggunakan kembali perilaku kelas tertentu, Anda bisa menjadikannya sebagai objek terpisah dalam variabel instan, dengan cara itu semua janji dan persyaratannya tidak dipaksakan pada Anda.
Terkadang, bahasa alami kita menunjukkan hubungan pewarisan: Mobil adalah kendaraan. Sepeda motor adalah kendaraan. Haruskah saya mendefinisikan kelas
Car
danMotorcycle
mewarisi dariVehicle
kelas? Desain berorientasi objek bukan tentang mirroring dunia nyata persis dalam kode kita. Kita tidak dapat dengan mudah menyandikan taksonomi kaya dari dunia nyata dalam kode sumber kita.Salah satu contohnya adalah masalah pemodelan bos-karyawan. Kami memiliki beberapa
Person
s, masing-masing dengan nama dan alamat. AnEmployee
adalahPerson
dan memiliki aBoss
. ABoss
juga aPerson
. Jadi haruskah saya membuatPerson
kelas yang diwarisi olehBoss
danEmployee
? Sekarang saya memiliki masalah: bos juga seorang karyawan dan memiliki atasan lain. Jadi sepertinyaBoss
harus diperluasEmployee
. TapiCompanyOwner
ituBoss
tapi bukanEmployee
? Apa pun jenis grafik warisan entah bagaimana akan rusak di sini.OOP bukan tentang hierarki, pewarisan, dan penggunaan kembali kelas yang ada, ini tentang generalisasi perilaku . OOP adalah tentang “Saya memiliki banyak objek dan ingin mereka melakukan hal tertentu - dan saya tidak peduli bagaimana caranya.” Itulah gunanya antarmuka . Jika Anda mengimplementasikan
Iterable
antarmuka untuk AndaNode
karena Anda ingin membuatnya dapat diubah, itu tidak masalah. Jika Anda mengimplementasikanCollection
antarmuka karena Anda ingin menambah / menghapus node anak dll, maka itu tidak masalah. Tetapi mewarisi dari kelas lain karena kebetulan memberi Anda semua yang tidak, atau setidaknya tidak kecuali Anda telah memikirkannya dengan seksama seperti diuraikan di atas.sumber
Memperluas wadah dengan sendirinya biasanya diterima sebagai praktik yang buruk. Benar-benar sangat sedikit alasan untuk memperpanjang wadah alih-alih hanya memiliki satu. Memperluas wadah dirimu hanya membuatnya menjadi aneh.
sumber
Menambah apa yang telah dikatakan, ada alasan khusus Jawa untuk menghindari struktur semacam ini.
Kontrak
equals
metode untuk daftar mengharuskan daftar untuk dianggap sama dengan objek lainSumber: https://docs.oracle.com/javase/7/docs/api/java/util/List.html#equals(java.lang.Object)
Secara khusus, ini berarti bahwa memiliki kelas yang dirancang sebagai daftar itu sendiri dapat membuat perbandingan kesetaraan menjadi mahal (dan perhitungan hash juga jika daftar dapat diubah), dan jika kelas memiliki beberapa bidang contoh, maka ini harus diabaikan dalam perbandingan kesetaraan .
sumber
Mengenai memori:
Saya akan mengatakan ini adalah masalah perfeksionisme. Konstruktor-standar dari
ArrayList
tampilan seperti ini:Sumber . Konstruktor ini juga digunakan dalam Oracle-JDK.
Sekarang pertimbangkan untuk membuat Daftar yang terhubung sendiri dengan kode Anda. Anda baru saja berhasil membengkak konsumsi memori Anda dengan faktor 10x (tepatnya bahkan sedikit lebih tinggi). Untuk pohon ini dapat dengan mudah menjadi sangat buruk juga tanpa persyaratan khusus pada struktur pohon. Menggunakan
LinkedList
kelas atau lainnya sebagai alternatif harus menyelesaikan masalah ini.Sejujurnya, dalam banyak kasus ini hanyalah perfeksionisme belaka, bahkan jika kita mengabaikan jumlah memori yang tersedia. A
LinkedList
akan memperlambat kode sedikit sebagai alternatif, jadi itu merupakan trade-off antara kinerja dan konsumsi memori. Masih pendapat pribadi saya tentang ini adalah untuk tidak menyia-nyiakan memori sebanyak itu dengan cara yang dapat dengan mudah dielakkan seperti ini.EDIT: klarifikasi tentang komentar (@amon tepatnya). Bagian jawaban ini tidak membahas masalah warisan. Perbandingan penggunaan memori dibuat terhadap Daftar yang terhubung sendiri-sendiri dan dengan penggunaan memori terbaik (dalam implementasi sebenarnya faktor mungkin berubah sedikit, tetapi masih cukup besar untuk meringkas hingga cukup banyak memori yang terbuang).
Mengenai "Praktek buruk":
Pastinya. Ini bukan standar-cara menerapkan grafik untuk alasan sederhana: A Grafik-Node memiliki anak-node, bukan itu sebagai daftar anak-node. Tepatnya mengekspresikan apa yang Anda maksud dalam kode adalah keterampilan utama. Baik itu dalam nama variabel atau struktur yang mengekspresikan seperti ini. Poin berikutnya: menjaga antarmuka minimal: Anda telah membuat setiap metode
ArrayList
tersedia untuk pengguna kelas melalui warisan. Tidak ada cara untuk mengubah ini tanpa merusak seluruh struktur kode. Sebagai gantinya simpanList
variabel internal dan sediakan metode yang diperlukan melalui metode adaptor. Dengan cara ini Anda dapat dengan mudah menambah dan menghapus fungsionalitas dari kelas tanpa mengacaukan semuanya.sumber
new Object[0]
total menggunakan 4 kata,new Object[10]
hanya akan menggunakan memori 14 kata.ArrayList
memiliki cukup banyak memori.Ini terlihat seperti semantik dari pola komposit . Ini digunakan untuk memodelkan struktur data rekursif.
sumber
public class Node extends ArrayList<Node> { public string Item; }
adalah versi warisan dari jawaban Anda. Anda lebih mudah untuk bernalar, tetapi keduanya mencapai beberapa hal: mereka mendefinisikan pohon non-biner.