Tampaknya Java memiliki kekuatan untuk mendeklarasikan kelas yang tidak dapat diturunkan untuk waktu yang lama, dan sekarang C ++ juga memilikinya. Namun, mengingat prinsip Open / Close dalam SOLID, mengapa hal itu berguna? Bagi saya, final
kata kunci terdengar seperti friend
- itu legal, tetapi jika Anda menggunakannya, kemungkinan besar desainnya salah. Berikan beberapa contoh di mana kelas yang tidak dapat diturunkan akan menjadi bagian dari arsitektur atau pola desain yang hebat.
54
final
? Banyak orang (termasuk saya) menemukan bahwa ini adalah desain yang bagus untuk membuat setiap kelas non-abstrakfinal
.final
.Jawaban:
final
mengekspresikan niat . Ini memberi tahu pengguna kelas, metode atau variabel "Elemen ini tidak seharusnya berubah, dan jika Anda ingin mengubahnya, Anda belum memahami desain yang ada."Ini penting karena arsitektur program akan sangat, sangat sulit jika Anda harus mengantisipasi bahwa setiap kelas dan setiap metode yang pernah Anda tulis dapat diubah untuk melakukan sesuatu yang sama sekali berbeda dengan subkelas. Adalah jauh lebih baik untuk memutuskan di muka elemen mana yang seharusnya dapat diubah dan mana yang tidak, dan untuk menegakkan ketidakstabilan melalui
final
.Anda juga dapat melakukan ini melalui komentar dan dokumen arsitektur, tetapi selalu lebih baik membiarkan kompiler memberlakukan hal-hal yang dapat dilakukannya daripada berharap bahwa pengguna di masa depan akan membaca dan mematuhi dokumentasi.
sumber
Ini menghindari Masalah Kelas Basis Fragile . Setiap kelas dilengkapi dengan serangkaian jaminan dan invarian implisit atau eksplisit. Prinsip Substitusi Liskov mengamanatkan bahwa semua subtipe kelas itu juga harus memberikan semua jaminan ini. Namun, sangat mudah untuk melanggar ini jika kita tidak menggunakannya
final
. Misalnya, mari kita periksa pemeriksa kata sandi:Jika kita membiarkan kelas itu ditimpa, satu implementasi dapat mengunci semua orang, yang lain mungkin memberi semua orang akses:
Ini biasanya tidak OK, karena subclass sekarang memiliki perilaku yang sangat tidak sesuai dengan aslinya. Jika kita benar-benar ingin kelas diperluas dengan perilaku lain, Rantai Tanggung Jawab akan lebih baik:
Masalahnya menjadi lebih jelas ketika kelas yang lebih rumit memanggil metode sendiri, dan metode tersebut dapat diganti. Saya kadang-kadang menghadapi ini ketika cukup mencetak struktur data atau menulis HTML. Setiap metode bertanggung jawab atas beberapa widget.
Sekarang saya membuat subkelas yang menambahkan sedikit lebih banyak gaya:
Sekarang abaikan sejenak bahwa ini bukan cara yang sangat baik untuk menghasilkan halaman HTML, apa yang terjadi jika saya ingin mengubah tata letak lagi? Saya harus membuat
SpiffyPage
subclass yang entah bagaimana membungkus konten itu. Apa yang bisa kita lihat di sini adalah aplikasi pola pola templat yang tidak disengaja . Metode templat adalah titik ekstensi yang terdefinisi dengan baik di kelas dasar yang dimaksudkan untuk diganti.Dan apa yang terjadi jika kelas dasar berubah? Jika konten HTML berubah terlalu banyak, ini bisa merusak tata letak yang disediakan oleh subkelas. Karena itu tidak benar-benar aman untuk mengubah kelas dasar sesudahnya. Ini tidak jelas jika semua kelas Anda berada dalam proyek yang sama, tetapi sangat nyata jika kelas dasar adalah bagian dari beberapa perangkat lunak yang diterbitkan yang dibangun oleh orang lain.
Jika strategi ekstensi ini dimaksudkan, kami dapat memungkinkan pengguna untuk menukar cara masing-masing bagian dihasilkan. Baik, mungkin ada Strategi untuk setiap blok yang dapat disediakan secara eksternal. Atau, kita bisa bersarang Dekorator. Ini akan setara dengan kode di atas, tetapi jauh lebih eksplisit dan jauh lebih fleksibel:
(
top
Parameter tambahan diperlukan untuk memastikan bahwa panggilan haruswriteMainContent
melalui bagian atas rantai dekorator. Ini mengemulasi fitur subkelas yang disebut rekursi terbuka .)Jika kita memiliki beberapa dekorator, sekarang kita dapat mencampurnya dengan lebih bebas.
Jauh lebih sering daripada keinginan untuk sedikit menyesuaikan fungsionalitas yang ada adalah keinginan untuk menggunakan kembali beberapa bagian dari kelas yang ada. Saya telah melihat sebuah kasus di mana seseorang menginginkan kelas di mana Anda dapat menambahkan item dan mengulanginya semua. Solusi yang benar adalah:
Sebagai gantinya, mereka membuat subclass:
Ini tiba-tiba berarti bahwa seluruh antarmuka
ArrayList
telah menjadi bagian dari antarmuka kami . Pengguna dapatremove()
hal - hal, atauget()
hal - hal pada indeks tertentu. Ini dimaksudkan seperti itu? BAIK. Tetapi sering kali, kita tidak dengan hati-hati memikirkan semua konsekuensi.Oleh karena itu disarankan untuk
extend
kelas tanpa pemikiran yang cermat.final
kecuali kecuali jika Anda bermaksud untuk mengganti metode apa pun.Ada banyak contoh di mana "aturan" ini harus dilanggar, tetapi biasanya memandu Anda untuk desain yang baik, fleksibel, dan menghindari bug karena perubahan yang tidak diinginkan pada kelas dasar (atau penggunaan subkelas yang tidak diinginkan sebagai turunan dari kelas dasar ).
Beberapa bahasa memiliki mekanisme penegakan yang lebih ketat:
virtual
sumber
Saya terkejut bahwa belum ada yang menyebutkan Java Efektif, Edisi ke-2 oleh Joshua Bloch (yang harus dibaca setidaknya untuk setiap pengembang Java setidaknya). Butir 17 dalam buku ini membahas hal ini secara terperinci, dan berjudul: " Desain dan dokumen untuk warisan atau yang dilarang ".
Saya tidak akan mengulangi semua saran bagus dalam buku ini, tetapi paragraf-paragraf khusus ini tampaknya relevan:
sumber
Salah satu alasan
final
berguna adalah memastikan bahwa Anda tidak dapat membuat subkelas kelas dengan cara yang akan melanggar kontrak kelas induk. Subclassing seperti itu akan menjadi pelanggaran SOLID (terutama "L") dan membuat kelasfinal
mencegahnya.Salah satu contoh umum adalah membuatnya tidak mungkin untuk subkelas kelas yang tidak dapat diubah dengan cara yang akan membuat subkelas bisa berubah. Dalam kasus-kasus tertentu, perubahan perilaku semacam itu dapat menyebabkan efek yang sangat mengejutkan, misalnya ketika Anda menggunakan sesuatu sebagai kunci dalam peta yang berpikir bahwa kunci tersebut tidak dapat diubah sementara pada kenyataannya Anda menggunakan subkelas yang dapat diubah.
Di Jawa, banyak masalah keamanan yang menarik dapat diperkenalkan jika Anda dapat membuat subkelas
String
dan membuatnya bisa berubah (atau membuatnya menelepon kembali ke rumah ketika seseorang memanggil metode-metodenya, sehingga mungkin menarik data sensitif dari sistem) ketika benda-benda ini dilewatkan sekitar beberapa kode internal yang terkait dengan pemuatan kelas dan keamanan.Final juga terkadang membantu dalam mencegah kesalahan sederhana seperti menggunakan kembali variabel yang sama untuk dua hal dalam suatu metode, dll. Dalam Scala, Anda dianjurkan untuk hanya menggunakan
val
yang secara kasar sesuai dengan variabel final di Jawa, dan sebenarnya setiap penggunaanvar
atau variabel non-final dipandang dengan kecurigaan.Akhirnya, kompiler dapat, setidaknya secara teori, melakukan beberapa optimasi tambahan ketika mereka tahu bahwa suatu kelas atau metode adalah final, karena ketika Anda memanggil metode pada kelas akhir, Anda tahu persis metode mana yang akan dipanggil dan tidak harus pergi melalui tabel metode virtual untuk memeriksa warisan.
sumber
SecurityManager
. Sebagian besar program tidak menggunakannya, tetapi mereka juga tidak menjalankan kode yang tidak tepercaya. +++ Anda harus mengasumsikan bahwa string tidak dapat diubah karena jika tidak, Anda tidak akan mendapatkan keamanan nol dan banyak bug sewenang-wenang sebagai bonus. Setiap programmer dengan asumsi string Java dapat berubah dalam kode produktif pasti gila.Alasan kedua adalah kinerja . Alasan pertama adalah karena beberapa kelas memiliki perilaku atau keadaan penting yang tidak seharusnya diubah untuk memungkinkan sistem bekerja. Sebagai contoh jika saya memiliki kelas "Kata Sandi" dan untuk membangun kelas itu saya telah menyewa tim ahli keamanan dan kelas ini berkomunikasi dengan ratusan ATM dengan procol yang dipelajari dan didefinisikan dengan baik. Izinkan orang baru yang direkrut keluar dari universitas untuk membuat kelas "TrustMePasswordCheck" yang memperluas kelas di atas bisa sangat berbahaya bagi sistem saya; metode-metode itu tidak seharusnya diganti, itu saja.
sumber
Ketika saya membutuhkan kelas, saya akan menulis kelas. Jika saya tidak membutuhkan subclass, saya tidak peduli dengan subclass. Saya memastikan bahwa kelas saya berperilaku seperti yang dimaksudkan, dan tempat-tempat di mana saya menggunakan kelas menganggap bahwa kelas berperilaku sebagaimana dimaksud.
Jika ada yang ingin subkelas kelas saya, saya ingin sepenuhnya menolak tanggung jawab atas apa yang terjadi. Saya mencapainya dengan membuat kelas "final". Jika Anda ingin subkelasnya, ingatlah bahwa saya tidak memperhitungkan subklasifikasi saat saya menulis kelas. Jadi Anda harus mengambil kode sumber kelas, menghapus "final", dan sejak saat itu apa pun yang terjadi adalah sepenuhnya tanggung jawab Anda .
Anda pikir itu "tidak berorientasi objek"? Saya dibayar untuk membuat kelas yang melakukan apa yang seharusnya dilakukan. Tidak ada yang membayar saya untuk membuat kelas yang bisa disubklasifikasikan. Jika Anda dibayar untuk membuat kelas saya dapat digunakan kembali, Anda dapat melakukannya. Mulailah dengan menghapus kata kunci "final".
(Selain itu, "final" sering memungkinkan optimisasi substansial. Misalnya, dalam Swift "final" pada kelas publik, atau pada metode kelas publik, berarti kompiler dapat sepenuhnya mengetahui kode apa yang akan dijalankan oleh pemanggilan metode, dan dapat mengganti pengiriman dinamis dengan pengiriman statis (keuntungan kecil) dan sering mengganti pengiriman statis dengan inlining (mungkin manfaat besar)).
adelphus: Apa yang sulit dipahami tentang "jika Anda ingin mensubklasifikasikan, ambil kode sumber, hapus 'final', dan itu tanggung jawab Anda"? "final" sama dengan "peringatan adil".
Dan saya tidak dibayar untuk membuat kode yang dapat digunakan kembali. Saya dibayar untuk menulis kode yang melakukan apa yang seharusnya dilakukan. Jika saya dibayar untuk membuat dua bit kode yang serupa, saya mengekstrak bagian-bagian umum karena itu lebih murah dan saya tidak dibayar untuk membuang waktu saya. Membuat kode yang dapat digunakan kembali yang tidak digunakan kembali adalah buang-buang waktu saya.
M4ks: Anda selalu menjadikan segala sesuatu pribadi yang tidak seharusnya diakses dari luar. Sekali lagi, jika Anda ingin subkelas, Anda mengambil kode sumber, ubah hal-hal menjadi "dilindungi" jika Anda perlu, dan bertanggung jawab atas apa yang Anda lakukan. Jika Anda merasa perlu mengakses hal-hal yang saya tandai pribadi, Anda lebih baik tahu apa yang Anda lakukan.
Keduanya: Subclassing adalah bagian kecil dari kode penggunaan kembali. Membuat blok bangunan yang dapat diadaptasi tanpa subklasifikasi jauh lebih kuat dan sangat bermanfaat dari "final" karena pengguna blok dapat bergantung pada apa yang mereka dapatkan.
sumber
Mari kita bayangkan bahwa SDK untuk platform mengirim kelas berikut:
Aplikasi subkelas kelas ini:
Semua baik-baik saja dan baik-baik saja, tetapi seseorang yang bekerja pada SDK memutuskan bahwa melewatkan metode
get
adalah konyol, dan membuat antarmuka lebih baik memastikan untuk menerapkan kompatibilitas ke belakang.Semuanya tampak baik-baik saja, sampai aplikasi dari atas dikompilasi ulang dengan SDK baru. Tiba-tiba, metode get overriden tidak dipanggil lagi, dan permintaan tidak dihitung.
Ini disebut masalah kelas dasar yang rapuh, karena perubahan yang tampaknya tidak berbahaya menghasilkan pemecah subclass. Setiap kali perubahan metode apa yang dipanggil di dalam kelas dapat menyebabkan subclass rusak. Itu cenderung berarti bahwa hampir semua perubahan dapat menyebabkan subclass rusak.
Final mencegah siapa pun dari subclass kelas Anda. Dengan begitu, metode mana di dalam kelas yang dapat diubah tanpa khawatir bahwa di suatu tempat seseorang bergantung pada pemanggilan metode mana yang dilakukan.
sumber
Final secara efektif berarti bahwa kelas Anda aman untuk diubah di masa depan tanpa memengaruhi kelas berbasis warisan hilir (karena tidak ada kelas), atau masalah apa pun di sekitar keamanan utas kelas (Saya pikir ada kasus di mana kata kunci akhir pada suatu bidang mencegah beberapa thread berbasis jinx tinggi).
Final berarti Anda bebas mengubah cara kerja kelas Anda tanpa ada perubahan perilaku yang tidak disengaja merayap ke dalam kode orang lain yang bergantung pada Anda sebagai basis.
Sebagai contoh, saya menulis kelas yang disebut HobbitKiller, yang hebat, karena semua hobbit adalah penipu dan mungkin harus mati. Gosok itu, mereka semua pasti harus mati.
Anda menggunakan ini sebagai kelas dasar dan menambahkan metode baru yang hebat untuk menggunakan penyembur api, tetapi gunakan kelas saya sebagai basis karena saya memiliki metode yang hebat untuk menargetkan para hobbit (selain menjadi tricksie, mereka cepat), yang Anda gunakan untuk membantu mengarahkan penyembur api Anda.
Tiga bulan kemudian saya mengubah implementasi metode penargetan saya. Sekarang, di beberapa titik di masa depan ketika Anda memutakhirkan pustaka Anda, tanpa Anda ketahui, implementasi runtime aktual kelas Anda telah berubah secara mendasar karena perubahan dalam metode kelas super yang Anda andalkan (dan umumnya tidak mengontrol).
Jadi bagi saya untuk menjadi pengembang yang teliti, dan memastikan kelancaran kematian hobbit ke masa depan menggunakan kelas saya, saya harus sangat, sangat berhati-hati dengan perubahan apa pun yang saya buat untuk setiap kelas yang dapat diperluas.
Dengan menghilangkan kemampuan untuk memperluas kecuali dalam kasus-kasus di mana saya secara khusus berniat untuk memperpanjang kelas, saya menyelamatkan diri saya (dan semoga yang lain) banyak sakit kepala.
sumber
final
Penggunaan
final
tidak dengan cara apa pun merupakan pelanggaran prinsip SOLID. Sayangnya, sangat umum untuk menafsirkan Prinsip Terbuka / Tertutup ("entitas perangkat lunak harus terbuka untuk ekstensi tetapi ditutup untuk modifikasi") sebagai makna "daripada memodifikasi kelas, mensubklasifikasikan, dan menambahkan fitur baru". Ini bukan yang semula dimaksudkan olehnya, dan umumnya dianggap bukan menjadi pendekatan terbaik untuk mencapai tujuannya.Cara terbaik untuk mematuhi OCP adalah merancang titik ekstensi ke dalam kelas, dengan secara khusus memberikan perilaku abstrak yang diparameterisasi dengan menyuntikkan ketergantungan pada objek (misalnya menggunakan pola desain Strategi). Perilaku ini harus dirancang untuk menggunakan antarmuka sehingga implementasi baru tidak bergantung pada warisan.
Pendekatan lain adalah mengimplementasikan kelas Anda dengan API publiknya sebagai kelas abstrak (atau antarmuka). Anda kemudian dapat menghasilkan implementasi yang sama sekali baru yang dapat dihubungkan ke klien yang sama. Jika antarmuka baru Anda memerlukan perilaku yang mirip dengan aslinya, Anda dapat:
sumber
Bagi saya ini masalah desain.
Anggaplah saya memiliki program yang menghitung gaji untuk karyawan. Jika saya memiliki kelas yang mengembalikan jumlah hari kerja antara 2 tanggal berdasarkan negara (satu kelas untuk masing-masing negara), saya akan meletakkan final itu, dan memberikan metode bagi setiap perusahaan untuk menyediakan hari gratis hanya untuk kalender mereka.
Mengapa? Sederhana. Katakanlah seorang pengembang ingin mewarisi kelas dasar WorkingDaysUSA di kelas WorkingDaysUSAmyCompany perusahaan dan memodifikasinya untuk mencerminkan bahwa perusahaannya akan ditutup untuk mogok / pemeliharaan / apa pun alasan 2 Mars.
Perhitungan untuk pesanan dan pengiriman klien akan mencerminkan keterlambatan dan bekerja sesuai ketika di runtime mereka memanggil WorkingDaysUSAmyCompany.getWorkingDays (), tetapi apa yang terjadi ketika saya menghitung waktu liburan? Haruskah saya menambahkan 2nd mars sebagai hari libur untuk semua orang? Tidak. Tetapi karena programmer menggunakan warisan dan saya tidak melindungi kelas ini dapat menyebabkan kebingungan.
Atau katakanlah mereka mewarisi dan memodifikasi kelas untuk mencerminkan bahwa perusahaan ini tidak bekerja pada hari Sabtu di mana di negara mereka bekerja setengah waktu pada hari Sabtu. Kemudian gempa bumi, krisis listrik atau keadaan tertentu membuat presiden menyatakan 3 hari tidak bekerja seperti yang terjadi baru-baru ini di Venezuela. Jika metode kelas warisan sudah dikurangi setiap hari Sabtu, modifikasi saya pada kelas asli dapat menyebabkan mengurangi hari yang sama dua kali. Saya harus pergi ke setiap subclass pada setiap klien dan memverifikasi semua perubahan yang kompatibel.
Larutan? Jadikan kelas final dan sediakan metode addFreeDay (companyID mycompany, Date freeDay). Dengan begitu Anda yakin bahwa ketika Anda memanggil kelas WorkingDaysCountry itu kelas utama Anda dan bukan subkelas
sumber