Mengapa suatu kelas harus selain dari "abstrak" atau "final / disegel"?


29

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.


21
Karena itu disebut Open/Closed principle, bukanClosed Principle.
StuperUser

2
Apa jenis hal yang Anda tulis secara profesional? Mungkin sangat mempengaruhi pendapat Anda tentang masalah ini.

Yah, saya tahu beberapa pengembang platform yang menyegel segalanya, karena mereka ingin meminimalkan antarmuka warisan.
K ..

4
@StuperUser: Itu bukan alasan, itu basi. OP tidak bertanya apa itu, dia bertanya mengapa . Jika tidak ada alasan di balik suatu prinsip, tidak ada alasan untuk memperhatikannya.
Michael Shaw

1
Saya bertanya-tanya berapa banyak kerangka kerja UI akan rusak dengan mengubah kelas Window ke disegel.
Reactgular

Jawaban:


46

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.


17
+1 untuk paragraf terakhir. Saya sering menemukan terlalu banyak enkapsulasi di perpustakaan pihak ke-3 (atau bahkan kelas perpustakaan standar!) Menjadi penyebab rasa sakit yang lebih besar daripada enkapsulasi yang terlalu sedikit.
Mason Wheeler

5
Argumen yang biasa untuk menyegel kelas adalah bahwa Anda tidak dapat memprediksi banyak cara berbeda yang mungkin dapat menimpa klien dari kelas Anda, dan karena itu Anda tidak dapat membuat jaminan tentang perilakunya. Lihat blogs.msdn.com/b/ericlippert/archive/2004/01/22/…
Robert Harvey

12
@ RobertTarvey Saya akrab dengan argumen dan itu kedengarannya bagus secara teori. Anda sebagai perancang objek tidak dapat melihat bagaimana orang dapat memperluas kelas Anda - itulah sebabnya mereka tidak boleh dimeteraikan. Anda tidak dapat mendukung semuanya - baik-baik saja. Jangan. Tapi jangan mengambil opsi juga.
Michael

6
@RobertHarvey bukankah hanya orang yang melanggar LSP yang mendapatkan apa yang pantas mereka terima?
StuperUser

2
Saya juga bukan penggemar kelas tersegel, tetapi saya bisa melihat mengapa beberapa perusahaan seperti Microsoft (yang sering harus mendukung hal-hal yang tidak mereka hancurkan) menemukan mereka menarik. Pengguna kerangka kerja tidak harus memiliki pengetahuan tentang internal kelas.
Robert Harvey

11

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.


Tetapi Anda bisa menerapkan argumen yang sama untuk kelas yang bukan bagian dari antarmuka publik. Salah satu alasan utama untuk membuat final kelas adalah bahwa ia bertindak sebagai langkah keamanan untuk mencegah kode ceroboh. Argumen itu sama validnya untuk basis kode apa pun yang dibagikan oleh pengembang yang berbeda dari waktu ke waktu, atau bahkan jika Anda adalah satu-satunya pengembang. Itu hanya melindungi semantik kode.
DPM

Saya pikir gagasan bahwa kelas harus abstrak atau disegel adalah bagian dari prinsip yang lebih luas, yaitu bahwa seseorang harus menghindari menggunakan variabel tipe kelas instantiable. Penghindaran semacam itu akan memungkinkan untuk membuat jenis yang dapat digunakan oleh konsumen dari jenis tertentu, tanpa harus mewarisi semua anggota pribadi daripadanya. Sayangnya, desain semacam itu tidak benar-benar berfungsi dengan sintaksis konstruktor publik. Sebaliknya, kode harus diganti new List<Foo>()dengan sesuatu seperti List<Foo>.CreateMutable().
supercat

5

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.


5

Dua kasus umum di mana Anda membutuhkan vanilla, kelas-kelas yang tidak disegel:

  1. Teknis: Jika Anda memiliki hierarki yang lebih dari dua level , dan Anda ingin dapat membuat instantiate sesuatu di tengah.

  2. 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 Controlmelalui 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:

  1. Dalam sebagian besar keadaan, kelas penyegelan sering tidak penting untuk keselamatan, karena pewaris tidak dapat mengacaukan virtualfungsi Anda, kecuali implementasi antarmuka eksplisit.
  2. Terkadang alternatif yang cocok adalah membuat Konstruktor internalalih-alih menyegel kelas, yang memungkinkannya diwariskan di dalam basis kode Anda, tetapi tidak di luarnya.

4

Saya percaya alasan sebenarnya banyak orang merasa kelas harus final/ sealedadalah 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 :

  • Ketergantungan antara metode (metode mana yang memanggil, dll.)
  • Ketergantungan pada variabel lokal
  • Kontrak internal yang harus dihormati oleh kelas yang diperluas
  • Panggilan konvensi untuk setiap metode (mis. Ketika Anda menimpanya, apakah Anda memanggil superimplementasi? 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:

  • Pertama-tama untuk memastikan perluasan adalah ide yang baik ketika Anda adalah klien dari kode, dan untuk benar - benar menyukai komposisi daripada warisan.
  • Kedua untuk membaca manual secara keseluruhan (lagi sebagai klien) untuk memastikan Anda tidak mengabaikan sesuatu yang disebutkan.
  • Ketiga, ketika Anda menulis sepotong kode yang akan digunakan klien, tulis dokumentasi yang tepat untuk kode tersebut (ya, jauh). Sebagai contoh positif, saya bisa memberikan dokumen Apple iOS. Mereka tidak cukup bagi pengguna untuk selalu memperluas kelas mereka dengan benar, tetapi mereka setidaknya menyertakan beberapa informasi tentang warisan. Yang lebih dari yang bisa saya katakan untuk sebagian besar API.
  • Keempat, benar-benar mencoba memperluas kelas Anda sendiri, untuk memastikan itu berfungsi Saya adalah pendukung besar untuk menyertakan banyak sampel dan tes dalam API, dan ketika Anda melakukan tes, Anda mungkin juga menguji rantai pewarisan: mereka adalah bagian dari kontrak Anda setelah semua!
  • Kelima, dalam situasi ketika Anda ragu, tunjukkan bahwa kelas tidak dimaksudkan untuk diperpanjang dan melakukannya adalah ide yang buruk (tm). Tunjukkan bahwa Anda seharusnya tidak dimintai pertanggungjawaban atas penggunaan yang tidak diinginkan tersebut, tetapi tetap tidak menyegel kelas. Jelas, ini tidak mencakup kasus ketika kelas harus 100% disegel.
  • Akhirnya, saat menyegel kelas, sediakan antarmuka sebagai pengait perantara, sehingga klien dapat "menulis ulang" versi modifikasi kelasnya sendiri dan mengerjakan kelas 'tersegel' Anda. Dengan cara ini, ia dapat mengganti kelas yang disegel dengan implementasinya. Sekarang, ini harus jelas, karena longgar dalam bentuk paling sederhana, tetapi masih layak disebutkan.

Perlu juga disebutkan pertanyaan "filosofis" berikut: Apakah suatu kelas merupakan sealed/ finalbagian 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.


3

Kelas tidak boleh final / tertutup atau abstrak jika:

  • Ini berguna sendiri, yaitu bermanfaat untuk memiliki instance dari kelas itu.
  • Ini bermanfaat bagi kelas itu untuk menjadi kelas subclass / base dari kelas lain.

Misalnya, ambil ObservableCollection<T>kelas dalam C # . Hanya perlu menambahkan peningkatan acara ke operasi normal a Collection<T>, itulah sebabnya subkelas Collection<T>. Collection<T>adalah kelas yang layak sendiri, dan begitu juga itu ObservableCollection<T>.


Jika saya bertanggung jawab atas ini, saya mungkin akan melakukan CollectionBase(abstrak), Collection : CollectionBase(disegel), ObservableCollection : CollectionBase(disegel). Jika Anda melihat Koleksi <T> dengan cermat, Anda akan melihat itu adalah kelas abstrak setengah-berpasangan.
Nicolas Repiquet

2
Apa keuntungan yang Anda dapatkan dari memiliki tiga kelas, bukan hanya dua? Juga, bagaimana Collection<T>"kelas abstrak setengah keledai"?
FishBasketGordo

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. ObservableCollectionmewarisi dari Collectiondan tidak disegel, sehingga Anda dapat mewarisinya lagi. Dan Anda dapat mengakses Itemsproperti yang dilindungi, memungkinkan Anda untuk menambahkan item dalam koleksi tanpa meningkatkan acara ... Bagus.
Nicolas Repiquet

@NicolasRepiquet: Dalam banyak bahasa dan kerangka kerja, cara idiomatis normal untuk membuat objek mensyaratkan bahwa tipe variabel yang akan menahan objek sama dengan jenis instance yang dibuat. Dalam banyak kasus, penggunaan yang ideal adalah membagikan referensi ke tipe abstrak, tetapi itu akan memaksa banyak kode untuk menggunakan satu tipe untuk variabel dan parameter dan tipe konkret yang berbeda saat memanggil konstruktor. Hampir tidak mungkin, tapi agak canggung.
supercat

3

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.


1
Hal-hal yang disegel akhir dalam perangkat lunak memecahkan masalah "Saya merasa perlu untuk memaksakan kehendak saya pada pengelola kode ini di masa mendatang".
Kaz

Bukan final / menyegel sesuatu yang ditambahkan ke OOP, karena saya tidak ingat ada di sekitar ketika saya masih muda. Sepertinya fitur jenis setelah dipikirkan.
Reactgular

Finalisasi ada sebagai prosedur toolchain dalam sistem OOP sebelum OOP menjadi bodoh dengan bahasa seperti C ++ dan Java. Programmer bekerja di Smalltalk, Lisp dengan fleksibilitas maksimum: apa pun dapat diperpanjang, metode baru ditambahkan setiap saat. Kemudian, gambar yang dikompilasi dari sistem tunduk pada optimasi: asumsi dibuat bahwa sistem tidak akan diperpanjang oleh pengguna akhir, dan ini berarti bahwa pengiriman metode dapat dioptimalkan berdasarkan pada persediaan metode apa dan kelas ada sekarang .
Kaz

Saya tidak berpikir itu hal yang sama, karena ini hanya fitur optimisasi. Saya tidak ingat disegel berada di semua versi Jawa, tetapi saya bisa salah karena saya tidak menggunakannya terlalu banyak.
Reactgular

Hanya karena itu memanifestasikan dirinya sebagai beberapa pernyataan yang harus Anda masukkan ke dalam kode tidak berarti itu bukan hal yang sama.
Kaz

2

"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!"


1

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.


Apa yang kamu bicarakan Dengan cara apa kelas tes tidak boleh final / disegel / abstrak?

1

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:

Base
 Function1
  Customer1
  Customer2
 Function2
 ...

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 Tablekelas umum yang mengimplementasikan query tabel database sementara sub-kelas Tablemengimplementasikan detail spesifik dari setiap tabel dengan menggunakan a enumuntuk mendefinisikan bidang. Membiarkan enumimplement antarmuka yang didefinisikan di Tablekelas masuk akal.


1

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.


1
Ini adalah komentar yang baik, tetapi tidak langsung menjawab pertanyaan. Silakan mempertimbangkan untuk memperluas pemikiran Anda di sini.

1

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.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.