Mengapa kelas tidak seharusnya dirancang untuk menjadi "terbuka"?

44

Saat membaca berbagai pertanyaan Stack Overflow dan kode orang lain, konsensus umum tentang cara merancang kelas ditutup. Ini berarti bahwa secara default di Java dan C # semuanya bersifat pribadi, bidang bersifat final, beberapa metode bersifat final, dan terkadang kelas bahkan final .

Gagasan di balik ini adalah untuk menyembunyikan detail implementasi, yang merupakan alasan yang sangat bagus. Namun dengan adanya protectedsebagian besar bahasa OOP dan polimorfisme, ini tidak berhasil.

Setiap kali saya ingin menambah atau mengubah fungsionalitas ke kelas saya biasanya terhalang oleh pribadi dan akhir ditempatkan di mana-mana. Di sini detail implementasi penting: Anda mengambil implementasi dan memperluasnya, mengetahui sepenuhnya apa konsekuensinya. Namun karena saya tidak bisa mendapatkan akses ke bidang dan metode pribadi dan terakhir, saya punya tiga opsi:

  • Jangan memperluas kelas, hanya menyelesaikan masalah yang mengarah ke kode yang lebih kompleks
  • Salin dan tempel seluruh kelas, matikan penggunaan kembali kode
  • Garap proyek

Itu bukan pilihan yang baik. Mengapa tidak protecteddigunakan dalam proyek yang ditulis dalam bahasa yang mendukungnya? Mengapa beberapa proyek secara eksplisit melarang pewarisan dari kelas mereka?

TheLQ
sumber
1
Saya setuju, saya punya masalah dengan komponen Java Swing, ini benar-benar buruk.
Jonas
3
Ada kemungkinan besar bahwa jika Anda mengalami masalah ini kelas-kelasnya dirancang dengan buruk - atau mungkin Anda mencoba menggunakannya secara salah. Anda tidak meminta kelas untuk informasi, Anda memintanya untuk melakukan sesuatu untuk Anda - karena itu Anda biasanya tidak memerlukan data itu. Meskipun ini tidak selalu benar, jika Anda merasa perlu mengakses data kelas, kemungkinan besar ada sesuatu yang salah.
Bill K
2
"sebagian besar bahasa OOP?" Saya tahu lebih banyak di mana kelas tidak bisa ditutup. Anda meninggalkan opsi keempat: ganti bahasa.
kevin cline
@Jonas Salah satu masalah utama Swing adalah terlalu banyak penerapannya. Itu benar-benar membunuh pembangunan.
Tom Hawtin - tackline

Jawaban:

55

Merancang kelas agar berfungsi dengan baik saat diperluas, terutama ketika programmer melakukan perluasan tidak sepenuhnya memahami bagaimana kelas seharusnya bekerja, membutuhkan usaha ekstra. Anda tidak bisa hanya mengambil semua yang bersifat pribadi dan menjadikannya publik (atau dilindungi) dan menyebutnya "terbuka." Jika Anda mengizinkan orang lain untuk mengubah nilai variabel, Anda harus mempertimbangkan bagaimana semua nilai yang mungkin akan mempengaruhi kelas. (Bagaimana jika mereka mengatur variabel ke nol? Array kosong? Angka negatif?) Hal yang sama berlaku ketika mengizinkan orang lain untuk memanggil metode. Butuh pemikiran yang cermat.

Jadi tidak terlalu banyak kelas yang seharusnya tidak terbuka, tetapi kadang-kadang itu tidak sepadan dengan upaya untuk membuat mereka terbuka.

Tentu saja, juga mungkin bahwa para penulis perpustakaan hanya malas. Tergantung perpustakaan mana yang Anda bicarakan. :-)

Harun
sumber
13
Ya, inilah mengapa kelas disegel secara default. Karena Anda tidak dapat memprediksi banyak cara klien Anda dapat memperluas kelas, lebih aman untuk menyegelnya daripada berkomitmen untuk mendukung jumlah cara yang mungkin tidak terbatas dari kelas yang mungkin diperluas.
Robert Harvey
18
Anda tidak harus memprediksi bagaimana kelas Anda akan ditimpa, Anda hanya harus berasumsi bahwa orang-orang yang memperluas kelas Anda tahu apa yang mereka lakukan. Jika mereka memperluas kelas dan mengatur sesuatu ke nol ketika seharusnya tidak nol maka itu adalah kesalahan mereka, bukan milikmu. Satu-satunya tempat yang masuk akal argumen ini adalah dalam aplikasi super kritis di mana sesuatu akan menjadi sangat salah jika bidang adalah nol.
TheLQ
5
@TheLQ: Itu asumsi yang cukup besar.
Robert Harvey
9
@TheLQ Salah siapa itu adalah pertanyaan yang sangat subjektif. Jika mereka menetapkan variabel ke nilai yang tampaknya masuk akal, dan Anda tidak mendokumentasikan fakta bahwa itu tidak diizinkan, dan kode Anda tidak melempar ArgumentException (atau setara), tetapi itu menyebabkan server Anda menjadi offline atau mengarah untuk eksploitasi keamanan, maka saya akan mengatakan itu adalah kesalahan Anda sendiri. Dan jika Anda benar-benar mendokumentasikan nilai-nilai yang valid dan mengajukan argumen, maka Anda telah melakukan upaya yang saya bicarakan, dan Anda harus bertanya pada diri sendiri apakah upaya itu benar-benar memanfaatkan waktu Anda.
Aaron
24

Membuat semuanya pribadi secara default terdengar kasar, tetapi lihatlah dari sisi lain: Ketika semuanya bersifat pribadi secara default, membuat sesuatu menjadi publik (atau dilindungi, yang merupakan hal yang hampir sama) seharusnya menjadi pilihan sadar; itu adalah kontrak penulis kelas dengan Anda, konsumen, tentang cara menggunakan kelas. Ini adalah kenyamanan bagi Anda berdua: Penulis bebas untuk memodifikasi bagian dalam kelas, selama antarmuka tetap tidak berubah; dan Anda tahu persis bagian mana dari kelas yang dapat Anda andalkan dan mana yang dapat berubah.

Gagasan yang mendasarinya adalah 'kopling longgar' (juga disebut sebagai 'antarmuka sempit'); nilainya terletak pada menjaga kompleksitas tetap rendah. Dengan mengurangi jumlah cara di mana komponen dapat berinteraksi, jumlah ketergantungan silang di antara mereka juga berkurang; dan lintas ketergantungan adalah salah satu jenis kompleksitas terburuk dalam hal pemeliharaan dan manajemen perubahan.

Di perpustakaan yang dirancang dengan baik, kelas-kelas yang layak diperluas melalui warisan telah melindungi dan anggota publik di tempat yang tepat, dan menyembunyikan yang lainnya.

tammmer
sumber
Itu masuk akal. Masih ada masalah ketika Anda membutuhkan sesuatu yang sangat mirip tetapi dengan 1 atau 2 hal yang diubah dalam implementasi (inner kelas). Saya juga berjuang untuk memahami bahwa jika Anda mengesampingkan kelas, bukankah Anda sudah sangat bergantung pada implementasinya?
TheLQ
5
+1 untuk manfaat kopling longgar. @TheLQ, antarmuka yang harus Anda andalkan, bukan implementasinya.
Karl Bielefeldt
19

Segala sesuatu yang tidak pribadi dianggap lebih atau kurang ada dengan perilaku yang tidak berubah di setiap versi kelas yang akan datang. Bahkan, itu dapat dianggap sebagai bagian dari API, didokumentasikan atau tidak. Oleh karena itu, mengekspos terlalu banyak detail mungkin menyebabkan masalah kompatibilitas nanti.

Mengenai resp "final". "segel" kelas, satu kasus khas adalah kelas abadi. Banyak bagian kerangka bergantung pada string yang tidak dapat diubah. Jika kelas String tidak final, akan mudah (bahkan menggoda) untuk membuat subkelas String yang dapat berubah yang menyebabkan semua jenis bug di banyak kelas lain saat digunakan alih-alih kelas String yang tidak dapat diubah.

pengguna281377
sumber
4
Ini jawaban yang sebenarnya. Jawaban lain menyentuh pada hal-hal penting tetapi bit yang relevan sebenarnya adalah ini: segala sesuatu yang tidak finaldan / atau privateterkunci pada tempatnya dan tidak pernah dapat diubah .
Konrad Rudolph
1
@Konrad: Sampai para pengembang memutuskan untuk mengubah segalanya dan tidak kompatibel dengan mundur.
JAB
Itu benar, tetapi perlu dicatat bahwa persyaratannya jauh lebih lemah daripada publicanggota; protectedanggota membentuk kontrak hanya antara kelas yang memaparkan mereka dan kelas turunan langsungnya; sebaliknya, publicanggota untuk kontrak dengan semua konsumen atas nama semua kelas warisan yang ada sekarang dan yang akan datang.
supercat
14

Di OO ​​ada dua cara untuk menambahkan fungsionalitas ke kode yang ada.

Yang pertama adalah berdasarkan warisan: Anda mengambil kelas dan berasal dari itu. Namun, warisan harus digunakan dengan hati-hati. Anda harus menggunakan warisan publik terutama ketika Anda memiliki hubungan isA antara basis dan kelas turunan (misalnya Rectangle is a Shape). Sebaliknya, Anda harus menghindari warisan publik untuk menggunakan kembali implementasi yang ada. Pada dasarnya, warisan publik digunakan untuk memastikan kelas turunan memiliki antarmuka yang sama dengan kelas dasar (atau yang lebih besar), sehingga Anda dapat menerapkan prinsip substitusi Liskov .

Metode lain untuk menambah fungsionalitas adalah menggunakan delegasi atau komposisi. Anda menulis kelas Anda sendiri yang menggunakan kelas yang ada sebagai objek internal, dan mendelegasikan bagian dari pekerjaan implementasi untuk itu.

Saya tidak yakin apa ide di balik semua final di perpustakaan yang Anda coba gunakan. Mungkin para pemrogram ingin memastikan Anda tidak akan mewarisi kelas baru dari kelas mereka, karena itu bukan cara terbaik untuk memperluas kode mereka.

meringkuk
sumber
5
Poin bagus dengan komposisi vs warisan.
Zsolt Török
+1, tapi saya tidak pernah menyukai contoh "adalah bentuk" untuk warisan. Persegi panjang dan lingkaran bisa menjadi dua hal yang sangat berbeda. Saya lebih suka menggunakan, persegi adalah persegi panjang yang dibatasi. Persegi panjang dapat digunakan seperti Bentuk.
tylermac
@tylermac: memang. Kasus Square / Rectangle sebenarnya adalah salah satu contoh tandingan dari LSP. Mungkin, saya harus mulai memikirkan contoh yang lebih efektif.
knulp
3
Kotak / Kotak hanya merupakan contoh balasan jika Anda mengizinkan modifikasi.
starblue
11

Sebagian besar jawabannya benar: Kelas harus disegel (final, NotOverridable, dll) ketika objek tidak dirancang atau dimaksudkan untuk diperpanjang.

Namun, saya tidak akan mempertimbangkan hanya menutup semua desain kode yang tepat. "O" dalam SOLID adalah untuk "Prinsip Terbuka-Tertutup", yang menyatakan bahwa kelas harus "ditutup" untuk modifikasi, tetapi "terbuka" untuk ekstensi. Idenya adalah, Anda memiliki kode dalam suatu objek. Ini bekerja dengan baik. Menambahkan fungsionalitas seharusnya tidak mengharuskan pembukaan kode itu dan membuat perubahan bedah yang dapat merusak perilaku kerja sebelumnya. Sebaliknya, seseorang harus dapat menggunakan pewarisan atau injeksi ketergantungan untuk "memperluas" kelas untuk melakukan hal-hal tambahan.

Sebagai contoh, sebuah kelas "ConsoleWriter" dapat memperoleh teks dan menampilkannya ke konsol. Itu melakukan ini dengan baik. Namun, dalam kasus tertentu Anda JUGA membutuhkan output yang sama ditulis ke file. Membuka kode ConsoleWriter, mengubah antarmuka eksternal untuk menambahkan parameter "WriteToFile" ke fungsi utama, dan menempatkan kode penulisan file tambahan di sebelah kode penulisan konsol akan dianggap sebagai hal yang buruk.

Sebagai gantinya, Anda bisa melakukan salah satu dari dua hal: Anda bisa berasal dari ConsoleWriter untuk membentuk ConsoleAndFileWriter, dan perluas metode Write () untuk memanggil implementasi basis, pertama-tama juga menulis ke file. Atau, Anda bisa mengekstrak antarmuka IWriter dari ConsoleWriter, dan mengimplementasikan kembali antarmuka itu untuk membuat dua kelas baru; FileWriter, dan "MultiWriter" yang dapat diberikan IWriters lain dan akan "menyiarkan" panggilan apa pun yang dibuat untuk metodenya kepada semua penulis yang diberikan. Yang mana yang Anda pilih tergantung pada apa yang akan Anda butuhkan; derivasi sederhana adalah, well, lebih sederhana, tetapi jika Anda memiliki pandangan ke depan yang suram pada akhirnya perlu mengirim pesan ke klien jaringan, tiga file, dua pipa bernama dan konsol, silakan mengambil kesulitan untuk mengekstrak antarmuka dan membuat "Y-adapter"; saya t'

Sekarang, jika fungsi Write () tidak pernah dinyatakan virtual (atau disegel), sekarang Anda dalam masalah jika Anda tidak mengontrol kode sumber. Terkadang bahkan jika Anda melakukannya. Ini pada umumnya posisi yang buruk, dan itu membuat para pengguna API sumber-tertutup atau sumber-terbatas menjadi frustasi ketika itu terjadi. Tapi, ada alasan yang sah mengapa Write (), atau seluruh kelas ConsoleWriter, disegel; mungkin ada informasi sensitif di bidang yang dilindungi (diganti secara bergantian dari kelas dasar untuk memberikan informasi tersebut). Mungkin tidak ada validasi yang memadai; ketika Anda menulis api, Anda harus mengasumsikan programmer yang mengkonsumsi kode Anda tidak lebih pintar atau baik daripada rata-rata "pengguna akhir" dari sistem akhir; dengan begitu kamu tidak pernah kecewa.

KeithS
sumber
Poin bagus. Warisan bisa baik atau buruk, jadi salah jika mengatakan bahwa setiap kelas harus dimeteraikan. Itu benar-benar tergantung pada masalah yang dihadapi.
knulp
2

Saya pikir itu mungkin dari semua orang yang menggunakan bahasa oo untuk pemrograman c. Aku melihatmu, java. Jika palu Anda berbentuk seperti objek, tetapi Anda menginginkan sistem prosedural, dan / atau tidak benar - benar memahami kelas, maka proyek Anda perlu dikunci.

Pada dasarnya, jika Anda akan menulis dalam bahasa oo, proyek Anda perlu mengikuti paradigma oo, yang mencakup ekstensi. Tentu saja, jika seseorang menulis perpustakaan dalam paradigma yang berbeda, mungkin Anda tidak seharusnya memperpanjangnya.

Spencer Rathbun
sumber
7
BTW, OO dan prosedural yang paling tegas TIDAK saling eksklusif. Anda tidak dapat memiliki OO tanpa prosedur. Juga, komposisi sama seperti konsep OO sebagai ekstensi.
Michael K
@Michael tentu saja tidak. Saya menyatakan bahwa Anda mungkin menginginkan sistem prosedural , yaitu, satu tanpa konsep OO. Saya telah melihat orang menggunakan Java untuk menulis kode prosedur murni, dan itu tidak berfungsi jika Anda mencoba dan berinteraksi dengannya secara OO.
Spencer Rathbun
1
Itu bisa diselesaikan, jika Java memiliki fungsi kelas satu. Meh
Michael K
Sulit untuk mengajarkan Bahasa Java atau DOTNet kepada siswa baru. Saya biasanya mengajar Prosedural dengan Prosedural Pascal atau "Plain C", dan kemudian, beralih ke OO dengan Object Pascal atau "C ++". Dan, tinggalkan Java untuk nanti. Pengajaran pemrograman dengan program prosedural terlihat seperti objek tunggal (singleton) bekerja dengan baik.
umlcat
@Michael K: Dalam OOP fungsi kelas satu hanyalah sebuah objek dengan tepat satu metode.
Giorgio
2

Karena mereka tidak tahu yang lebih baik.

Penulis asli mungkin berpegang teguh pada kesalahpahaman prinsip SOLID yang berasal dari dunia C ++ yang bingung dan rumit.

Saya harap Anda akan melihat bahwa dunia ruby, python, dan perl tidak memiliki masalah yang jawaban di sini diklaim sebagai alasan penyegelan. Perhatikan bahwa pengetikannya ortogonal ke dinamis. Pengubah akses mudah digunakan di sebagian besar (semua?) Bahasa. Kolom C ++ dapat dipermainkan dengan melakukan casting ke beberapa tipe lain (C ++ lebih lemah). Java dan C # dapat menggunakan refleksi. Akses pengubah membuat hal-hal cukup sulit untuk mencegah Anda melakukannya kecuali Anda BENAR-BENAR menginginkannya.

Menyegel kelas dan menandai anggota mana pun secara pribadi melanggar prinsip bahwa hal-hal sederhana harus sederhana dan hal-hal sulit harus dimungkinkan. Tiba-tiba hal-hal yang seharusnya sederhana, bukan.

Saya mendorong Anda untuk mencoba memahami sudut pandang penulis asli. Sebagian besar darinya berasal dari gagasan akademis enkapsulasi yang tidak pernah menunjukkan keberhasilan absolut di dunia nyata. Saya belum pernah melihat kerangka kerja atau pustaka di mana beberapa pengembang di suatu tempat tidak berharap itu bekerja sedikit berbeda dan tidak punya alasan yang baik untuk mengubahnya. Ada dua kemungkinan yang mungkin menjangkiti pengembang perangkat lunak asli yang menyegel dan menjadikan anggota sebagai pribadi.

  1. Kesombongan - mereka benar-benar percaya bahwa mereka terbuka untuk perpanjangan dan ditutup untuk modifikasi
  2. Complacence - mereka tahu mungkin ada use case lain tetapi memutuskan untuk tidak menulis untuk use case tersebut

Saya pikir dalam kerangka kerja perusahaan, # 2 mungkin demikian. Kerangka kerja C ++, Java dan .NET harus "selesai" dan harus mengikuti panduan tertentu. Pedoman tersebut biasanya berarti tipe tersegel kecuali tipe tersebut secara eksplisit dirancang sebagai bagian dari hierarki tipe dan anggota pribadi untuk banyak hal yang mungkin berguna untuk digunakan orang lain .. tetapi karena mereka tidak secara langsung berhubungan dengan kelas itu, mereka tidak terkena . Mengekstraksi tipe baru akan terlalu mahal untuk didukung, didokumentasikan, dll ...

Seluruh ide di balik pengubah akses adalah bahwa pemrogram harus dilindungi dari diri mereka sendiri. "Pemrograman C buruk karena memungkinkan Anda menembak diri sendiri." Ini bukan filosofi yang saya setujui sebagai programmer.

Saya lebih suka pendekatan mangling nama python. Anda dapat dengan mudah (jauh lebih mudah daripada refleksi) mengganti kemaluan jika Anda membutuhkan. Langgan yang bagus tersedia di sini: http://bytebaker.com/2009/03/31/python-properties-vs-java-access-modifiers/

Pengubah pribadi Ruby sebenarnya lebih seperti dilindungi dalam C # dan tidak memiliki pribadi sebagai pengubah C #. Dilindungi sedikit berbeda. Ada banyak penjelasan di sini: http://www.ruby-lang.org/en/documentation/ruby-from-other-languages/

Ingat, bahasa statis Anda tidak harus sesuai dengan gaya kuno kode writte di masa lalu bahasa itu.

Jrwren
sumber
5
"Enkapsulasi tidak pernah menunjukkan keberhasilan absolut". Saya akan berpikir bahwa semua orang akan setuju bahwa kurangnya enkapsulasi telah menyebabkan banyak masalah.
Yoh
5
-1. Orang-orang bodoh bergegas masuk ke mana malaikat takut melangkah. Merayakan kemampuan untuk mengubah variabel pribadi menunjukkan kelemahan serius dalam gaya pengkodean Anda.
riwalk
2
Selain bisa diperdebatkan dan mungkin salah, postingan ini juga cukup sombong dan menghina. Bagus sekali.
Konrad Rudolph
1
Ada dua aliran pemikiran yang pendukungnya tampaknya tidak rukun. Rakyat mengetik statis ingin mudah untuk alasan tentang perangkat lunak, rakyat dinamis ingin mudah untuk menambahkan fungsionalitas. Mana yang lebih baik pada dasarnya tergantung pada umur proyek yang diharapkan.
Yoh
1

EDIT: Saya percaya kelas harus dirancang agar terbuka. Dengan beberapa batasan, tetapi tidak boleh ditutup untuk warisan.

Mengapa: "Kelas akhir" atau "kelas tertutup" bagi saya terasa aneh. Saya tidak pernah harus menandai salah satu kelas saya sebagai "final" ("disegel"), karena saya mungkin harus mewarisi kelas itu, nanti.

Saya telah membeli / mengunduh perpustakaan pihak ketiga dengan kelas, (sebagian besar kontrol visual), dan sangat berterima kasih tidak ada perpustakaan yang menggunakan "final", bahkan jika didukung oleh bahasa pemrograman, di mana mereka diberi kode, karena saya akhirnya memperluas kelas-kelas itu, bahkan jika saya telah mengkompilasi perpustakaan tanpa kode sumber.

Kadang-kadang, saya harus berurusan dengan beberapa kelas "disegel", dan akhirnya membuat kelas baru "pembungkus" yang bersaing dengan kelas yang diberikan, yang memiliki anggota yang serupa.

class MyBaseWrapper {
  protected FinalClass _FinalObject;

  public virtual DoSomething()
  {
    // these method cannot be overriden,
    // its from a "final" class
    // the wrapper method can  be open and virtual:
    FinalObject.DoSomething();
  }
} // class MyBaseWrapper


class MyBaseWrapper: MyBaseWrapper {

  public override DoSomething()
  {
    // the wrapper method allows "overriding",
    // a method from a "final" class:
    DoSomethingNewBefore();
    FinalObject.DoSomething();
    DoSomethingNewAfter();
  }
} // class MyBaseWrapper

Clasifiers lingkup memungkinkan untuk "menyegel" beberapa fitur tanpa membatasi pewarisan.

Ketika saya beralih dari Structured Progr. untuk OOP, saya mulai menggunakan anggota kelas privat , tetapi, setelah banyak proyek, saya akhirnya menggunakan yang dilindungi , dan, akhirnya, mempromosikan properti atau metode itu ke publik , jika perlu.

PS Saya tidak suka "final" sebagai kata kunci dalam C #, saya lebih suka menggunakan "disegel" seperti Java, atau "steril", mengikuti metafora pewarisan. Selain itu, "final" adalah kata ambiguos yang digunakan dalam beberapa konteks.

umlcat
sumber
-1: Anda menyatakan Anda suka desain terbuka, tetapi ini tidak menjawab pertanyaan, itulah sebabnya mengapa kelas tidak boleh dirancang untuk terbuka.
Yoh
@ John Maaf, saya tidak menjelaskan diri saya dengan baik, saya mencoba untuk mengungkapkan pendapat yang berlawanan, seharusnya tidak ada yang disegel. Terima kasih telah menjelaskan mengapa Anda tidak setuju.
umlcat