Setelah 10+ tahun pemrograman java / c #, saya menemukan diri saya membuat:
- kelas abstrak : kontrak tidak dimaksudkan untuk dipakai seperti apa adanya.
- kelas final / tertutup : implementasi tidak dimaksudkan sebagai kelas dasar untuk sesuatu yang lain.
Saya tidak dapat memikirkan situasi di mana "kelas" sederhana (yaitu abstrak atau final / disegel) akan menjadi "pemrograman bijak".
Mengapa kelas harus selain "abstrak" atau "final / disegel"?
EDIT
Artikel hebat ini menjelaskan kekhawatiran saya jauh lebih baik daripada yang saya bisa.
object-oriented
programming-practices
Nicolas Repiquet
sumber
sumber
Open/Closed principle
, bukanClosed Principle.
Jawaban:
Ironisnya, saya menemukan yang sebaliknya: penggunaan kelas abstrak adalah pengecualian daripada aturan dan saya cenderung tidak menyukai kelas akhir / tertutup.
Antarmuka adalah mekanisme desain-kontrak-yang lebih khas karena Anda tidak menentukan internal - Anda tidak khawatir tentang mereka. Ini memungkinkan setiap implementasi kontrak itu menjadi independen. Ini adalah kunci dalam banyak domain. Misalnya, jika Anda sedang membangun ORM, akan sangat penting bagi Anda untuk meneruskan kueri ke database dengan cara yang seragam, tetapi implementasinya bisa sangat berbeda. Jika Anda menggunakan kelas abstrak untuk tujuan ini, Anda berakhir dengan perkabelan dalam komponen yang mungkin atau mungkin tidak berlaku untuk semua implementasi.
Untuk kelas final / tersegel, satu-satunya alasan yang dapat saya lihat untuk menggunakannya adalah ketika sebenarnya berbahaya untuk mengizinkan overriding - mungkin algoritma enkripsi atau sesuatu. Selain itu, Anda tidak pernah tahu kapan Anda ingin memperluas fungsionalitas karena alasan lokal. Sealing kelas membatasi opsi Anda untuk keuntungan yang tidak ada di sebagian besar skenario. Jauh lebih fleksibel untuk menulis kelas-kelas Anda sedemikian rupa sehingga kelas-kelas itu dapat diperluas di kemudian hari.
Pandangan terakhir ini telah disemen untuk saya dengan bekerja dengan komponen pihak ke-3 yang menyegel kelas sehingga mencegah beberapa integrasi yang akan membuat hidup jauh lebih mudah.
sumber
Itu adalah artikel yang bagus oleh Eric Lippert, tapi saya pikir itu tidak mendukung sudut pandang Anda.
Dia berpendapat bahwa semua kelas yang diekspos untuk digunakan oleh orang lain harus disegel, atau dibuat non-extensible.
Premis Anda adalah bahwa semua kelas harus abstrak atau disegel.
Perbedaan besar.
Artikel EL tidak mengatakan apa-apa tentang (mungkin banyak) kelas yang diproduksi oleh timnya yang Anda dan saya tidak tahu apa-apa tentang. Secara umum, kelas-kelas yang terpapar secara publik dalam suatu kerangka kerja hanya sebagian dari semua kelas yang terlibat dalam penerapan kerangka kerja itu.
sumber
new List<Foo>()
dengan sesuatu sepertiList<Foo>.CreateMutable()
.Dari perspektif java saya pikir kelas final tidak sepintar kelihatannya.
Banyak alat (terutama AOP, JPA dll) bekerja dengan waktu buka tunggu sehingga mereka harus memperpanjang klasimu. Cara lain adalah dengan membuat delegasi (bukan yang .NET) mendelegasikan segalanya ke kelas asli yang akan jauh lebih berantakan daripada memperluas kelas pengguna.
sumber
Dua kasus umum di mana Anda membutuhkan vanilla, kelas-kelas yang tidak disegel:
Teknis: Jika Anda memiliki hierarki yang lebih dari dua level , dan Anda ingin dapat membuat instantiate sesuatu di tengah.
Prinsip: Kadang-kadang diinginkan untuk menulis kelas yang eksplisitas dirancang untuk diperpanjang dengan aman . (Ini sering terjadi ketika Anda menulis API seperti Eric Lippert, atau ketika Anda bekerja dalam tim di proyek besar). Kadang-kadang Anda ingin menulis kelas yang berfungsi dengan baik sendiri, tetapi dirancang dengan pemikiran yang dapat diperpanjang.
Pemikiran Eric Lippert tentang pemeteraian masuk akal, tetapi ia juga mengakui bahwa mereka memang mendesain untuk perpanjangan dengan meninggalkan kelas "terbuka".
Ya, banyak kelas dimeteraikan di BCL, tetapi sejumlah besar kelas tidak, dan dapat diperpanjang dengan segala macam cara yang indah. Salah satu contoh yang muncul di pikiran adalah di Formulir Windows, di mana Anda dapat menambahkan data atau perilaku ke hampir semua
Control
melalui warisan. Tentu, ini bisa dilakukan dengan cara lain (pola dekorator, berbagai jenis komposisi, dll.), Tetapi pewarisan juga bekerja dengan sangat baik.Dua catatan khusus .NET:
virtual
fungsi Anda, kecuali implementasi antarmuka eksplisit.internal
alih-alih menyegel kelas, yang memungkinkannya diwariskan di dalam basis kode Anda, tetapi tidak di luarnya.sumber
Saya percaya alasan sebenarnya banyak orang merasa kelas harus
final
/sealed
adalah bahwa sebagian besar kelas diperpanjang non-abstrak tidak didokumentasikan dengan baik .Biarkan saya uraikan. Mulai dari jauh, ada pandangan di antara beberapa programmer bahwa pewarisan sebagai alat dalam OOP banyak digunakan dan disalahgunakan. Kita semua telah membaca prinsip substitusi Liskov, meskipun ini tidak menghentikan kita untuk melanggarnya ratusan (bahkan mungkin ribuan) kali.
Masalahnya adalah programmer suka menggunakan kembali kode. Bahkan ketika itu bukan ide yang bagus. Dan pewarisan adalah alat utama dalam kode "reabusing" ini. Kembali ke pertanyaan terakhir / tertutup.
Dokumentasi yang tepat untuk kelas akhir / tertutup relatif kecil: Anda menggambarkan apa yang dilakukan masing-masing metode, apa argumennya, nilai baliknya, dll. Semua hal yang biasa.
Namun, ketika Anda mendokumentasikan dengan benar kelas yang dapat diperpanjang, Anda harus memasukkan setidaknya yang berikut ini :
super
implementasi? Apakah Anda menyebutnya di awal metode, atau pada akhirnya? Pikirkan konstruktor vs destruktor)Ini hanya dari atas kepala saya. Dan saya dapat memberi Anda sebuah contoh mengapa masing-masing ini penting dan melewatkannya akan mengacaukan kelas yang diperluas.
Sekarang, pertimbangkan berapa banyak upaya dokumentasi yang harus dilakukan untuk mendokumentasikan dengan baik masing-masing hal ini. Saya percaya kelas dengan 7-8 metode (yang mungkin terlalu banyak di dunia ideal, tetapi terlalu sedikit di dunia nyata) mungkin juga memiliki dokumentasi sekitar 5 halaman, hanya teks. Jadi, sebagai gantinya, kita keluar setengah jalan dan tidak menyegel kelas, sehingga orang lain dapat menggunakannya, tetapi jangan mendokumentasikannya dengan baik, karena itu akan memakan banyak waktu (dan, Anda tahu, itu mungkin tidak pernah diperpanjang, jadi mengapa repot-repot?).
Jika Anda mendesain sebuah kelas, Anda mungkin merasakan godaan untuk menutupnya sehingga orang tidak dapat menggunakannya dengan cara yang tidak Anda perkirakan (dan siapkan untuk). Di sisi lain ketika Anda menggunakan kode orang lain, kadang-kadang dari API publik tidak ada alasan yang kelihatan untuk kelas menjadi final, dan Anda mungkin berpikir "Sial, ini hanya menghabiskan waktu 30 menit untuk mencari solusi".
Saya pikir beberapa elemen dari solusi adalah:
Perlu juga disebutkan pertanyaan "filosofis" berikut: Apakah suatu kelas merupakan
sealed
/final
bagian dari kontrak untuk kelas tersebut, atau detail implementasi? Sekarang saya tidak ingin menginjak sana, tetapi jawaban untuk ini juga harus mempengaruhi keputusan Anda apakah akan menutup kelas atau tidak.sumber
Kelas tidak boleh final / tertutup atau abstrak jika:
Misalnya, ambil
ObservableCollection<T>
kelas dalam C # . Hanya perlu menambahkan peningkatan acara ke operasi normal aCollection<T>
, itulah sebabnya subkelasCollection<T>
.Collection<T>
adalah kelas yang layak sendiri, dan begitu juga ituObservableCollection<T>
.sumber
CollectionBase
(abstrak),Collection : CollectionBase
(disegel),ObservableCollection : CollectionBase
(disegel). Jika Anda melihat Koleksi <T> dengan cermat, Anda akan melihat itu adalah kelas abstrak setengah-berpasangan.Collection<T>
"kelas abstrak setengah keledai"?Collection<T>
mengekspos banyak jeroan melalui metode dan properti yang dilindungi, dan jelas dimaksudkan sebagai kelas dasar untuk koleksi khusus. Tetapi tidak jelas di mana Anda seharusnya meletakkan kode Anda, karena tidak ada metode abstrak untuk mengimplementasikan.ObservableCollection
mewarisi dariCollection
dan tidak disegel, sehingga Anda dapat mewarisinya lagi. Dan Anda dapat mengaksesItems
properti yang dilindungi, memungkinkan Anda untuk menambahkan item dalam koleksi tanpa meningkatkan acara ... Bagus.Masalah dengan kelas akhir / disegel adalah bahwa mereka mencoba untuk menyelesaikan masalah yang belum terjadi. Ini berguna hanya ketika masalahnya ada, tetapi itu membuat frustrasi karena pihak ketiga telah memberlakukan pembatasan. Jarang kelas penyegelan memecahkan masalah saat ini, yang membuatnya sulit untuk membantah kegunaannya.
Ada kasus di mana kelas harus disegel. Sebagai contoh; Kelas mengelola sumber daya / memori yang dialokasikan dengan cara yang tidak dapat memprediksi bagaimana perubahan di masa depan dapat mengubah manajemen itu.
Selama bertahun-tahun saya telah menemukan enkapsulasi, callback, dan peristiwa jauh lebih fleksibel / berguna daripada kelas yang diabstraksi. Saya melihat jauh ke banyak kode dengan hierarki besar kelas di mana enkapsulasi dan acara akan membuat hidup lebih mudah bagi pengembang.
sumber
"Sealing" atau "finalisasi" dalam sistem objek memungkinkan untuk optimasi tertentu, karena grafik pengiriman lengkap diketahui.
Dengan kata lain, kami menyulitkan sistem untuk diubah menjadi sesuatu yang lain, sebagai pertukaran untuk kinerja. (Itulah inti dari sebagian besar optimasi.)
Dalam semua hal lain, ini adalah kerugian. Sistem harus terbuka dan dapat diperluas secara default. Seharusnya mudah untuk menambahkan metode baru ke semua kelas, dan memperluas secara sewenang-wenang.
Kami tidak mendapatkan fungsionalitas baru hari ini dengan mengambil langkah-langkah untuk mencegah ekstensi di masa mendatang.
Jadi jika kita melakukannya demi pencegahan itu sendiri, maka apa yang kita lakukan adalah mencoba mengendalikan kehidupan para penjaga masa depan. Ini tentang ego. "Bahkan ketika aku tidak lagi bekerja di sini, kode ini akan dipertahankan dengan caraku, sial!"
sumber
Kelas-kelas ujian sepertinya muncul di benak saya. Ini adalah kelas yang disebut dengan mode otomatis atau "sesuka" berdasarkan apa yang ingin dicapai oleh programmer / tester. Saya tidak yakin saya pernah melihat atau mendengar tentang tes kelas selesai yang bersifat pribadi.
sumber
Waktu ketika Anda perlu mempertimbangkan kelas yang dimaksudkan untuk diperpanjang adalah ketika Anda melakukan beberapa perencanaan nyata untuk masa depan. Biarkan saya memberi Anda contoh nyata dari pekerjaan saya.
Saya menghabiskan banyak waktu untuk menulis alat antarmuka antara produk utama kami dan sistem eksternal. Ketika kami melakukan penjualan baru, satu komponen besar adalah satu set eksportir yang dirancang untuk dijalankan secara berkala yang menghasilkan file data yang merinci peristiwa yang telah terjadi hari itu. File data ini kemudian dikonsumsi oleh sistem pelanggan.
Ini adalah peluang bagus untuk memperpanjang kelas.
Saya memiliki kelas Ekspor yang merupakan kelas dasar dari setiap eksportir. Ia tahu cara terhubung ke database, mencari tahu di mana ia harus terakhir kali berlari dan membuat arsip file data yang dihasilkannya. Ini juga menyediakan manajemen file properti, pencatatan dan beberapa penanganan pengecualian sederhana.
Selain itu, saya memiliki eksportir yang berbeda untuk bekerja dengan setiap jenis data, mungkin ada aktivitas pengguna, data transaksional, data manajemen kas dll.
Di atas tumpukan ini saya menempatkan lapisan khusus pelanggan yang mengimplementasikan struktur file data yang dibutuhkan pelanggan.
Dengan cara ini, eksportir dasar sangat jarang berubah. Eksportir tipe data inti terkadang berubah tetapi jarang, dan biasanya hanya menangani perubahan skema database yang harus disebarkan ke semua pelanggan. Satu-satunya pekerjaan yang harus saya lakukan untuk setiap pelanggan adalah bagian dari kode yang khusus untuk pelanggan itu. Dunia yang sempurna!
Jadi strukturnya terlihat seperti:
Poin utama saya adalah bahwa dengan membuat kode dengan cara ini saya dapat menggunakan pewarisan terutama untuk penggunaan kembali kode.
Saya harus mengatakan saya tidak bisa memikirkan alasan untuk melewati tiga lapisan.
Saya telah menggunakan dua layer berkali-kali, misalnya untuk memiliki
Table
kelas umum yang mengimplementasikan query tabel database sementara sub-kelasTable
mengimplementasikan detail spesifik dari setiap tabel dengan menggunakan aenum
untuk mendefinisikan bidang. Membiarkanenum
implement antarmuka yang didefinisikan diTable
kelas masuk akal.sumber
Saya telah menemukan kelas abstrak bermanfaat tetapi tidak selalu diperlukan, dan kelas tertutup menjadi masalah ketika menerapkan tes unit. Anda tidak dapat mengejek atau mematikan kelas tersegel, kecuali jika Anda menggunakan sesuatu seperti Teleriks justmock.
sumber
Saya setuju dengan pandangan Anda. Saya pikir di Java, secara default, kelas harus dinyatakan "final". Jika Anda tidak membuatnya final, maka secara khusus persiapkan dan dokumentasikan untuk perpanjangan.
Alasan utama untuk ini adalah untuk memastikan bahwa setiap instance dari kelas Anda akan mematuhi antarmuka yang Anda desain dan dokumentasikan. Kalau tidak, pengembang yang menggunakan kelas Anda berpotensi membuat kode rapuh dan tidak konsisten dan, pada gilirannya, meneruskannya ke pengembang / proyek lain, membuat objek kelas Anda tidak dapat dipercaya.
Dari perspektif yang lebih praktis memang ada kerugian untuk ini, karena klien dari antarmuka perpustakaan Anda tidak akan dapat melakukan tweaker dan menggunakan kelas Anda dengan cara yang lebih fleksibel yang awalnya Anda pikirkan.
Secara pribadi, untuk semua kode kualitas buruk yang ada (dan karena kita sedang membahas ini pada tingkat yang lebih praktis, saya berpendapat pengembangan Java lebih rentan terhadap ini) Saya pikir kekakuan ini adalah harga kecil yang harus dibayar dalam pencarian kami untuk lebih mudah pertahankan kode.
sumber