Banyak bahasa pemrograman yang paling populer (seperti C ++, Java, Python dll.) Memiliki konsep menyembunyikan / membayangi variabel atau fungsi. Ketika saya menemukan bersembunyi atau membayangi mereka telah menjadi penyebab sulitnya menemukan bug dan saya belum pernah melihat kasus di mana saya merasa perlu untuk menggunakan fitur-fitur bahasa ini.
Bagi saya akan lebih baik melarang persembunyian dan bayangan.
Adakah yang tahu tentang penggunaan konsep-konsep ini dengan baik?
Pembaruan:
Saya tidak merujuk pada enkapsulasi anggota kelas (anggota pribadi / yang dilindungi).
Jawaban:
Jika Anda melarang bersembunyi dan membayangi, apa yang Anda miliki adalah bahasa di mana semua variabel bersifat global.
Itu jelas lebih buruk daripada membiarkan variabel atau fungsi lokal yang dapat menyembunyikan variabel atau fungsi global.
Jika Anda melarang bersembunyi dan membayangi, DAN Anda mencoba untuk "melindungi" variabel global tertentu, Anda membuat situasi di mana kompiler memberi tahu programmer "Maaf, Dave, tetapi Anda tidak dapat menggunakan nama itu, itu sudah digunakan . " Pengalaman dengan COBOL menunjukkan bahwa programmer hampir segera menggunakan kata-kata kotor dalam situasi ini.
Masalah mendasar bukanlah menyembunyikan / membayangi, tetapi variabel global.
sumber
Menggunakan pengidentifikasi deskriptif yang akurat selalu bermanfaat.
Saya dapat berargumen bahwa menyembunyikan variabel tidak menyebabkan banyak bug karena memiliki dua variabel dengan nama yang sama / jenis yang sama (apa yang akan Anda lakukan jika menyembunyikan variabel tidak diizinkan) cenderung menyebabkan banyak bug dan / atau sama bug yang parah. Saya tidak tahu apakah argumen itu benar , tetapi setidaknya bisa diperdebatkan.
Menggunakan semacam notasi Hongaria untuk membedakan bidang vs variabel lokal mengatasi ini, tetapi memiliki dampak sendiri pada pemeliharaan (dan kewarasan programmer).
Dan (mungkin alasan utama mengapa konsep itu diketahui sejak awal) adalah jauh lebih mudah bagi bahasa untuk menerapkan persembunyian / pembayangan daripada melarangnya. Implementasi yang lebih mudah berarti bahwa kompiler cenderung memiliki bug. Implementasi yang lebih mudah berarti bahwa kompiler membutuhkan waktu lebih sedikit untuk menulis, menyebabkan adopsi platform lebih awal dan lebih luas.
sumber
Hanya untuk memastikan kita berada di halaman yang sama, metode "persembunyian" adalah ketika kelas turunan mendefinisikan anggota dengan nama yang sama dengan yang ada di kelas dasar (yang, jika itu adalah metode / properti, tidak ditandai virtual / dapat diganti) ), dan ketika dipanggil dari turunan kelas turunan dalam "konteks turunan", anggota turunan digunakan, sedangkan jika dipanggil oleh turunan yang sama dalam konteks kelas dasarnya, anggota kelas dasar digunakan. Ini berbeda dengan abstraksi anggota / pengesampingan di mana anggota kelas dasar mengharapkan kelas turunan untuk menentukan pengganti, dan dari pengubah lingkup / visibilitas yang "menyembunyikan" anggota dari konsumen di luar ruang lingkup yang diinginkan.
Jawaban singkat mengapa diizinkan adalah bahwa tidak melakukannya akan memaksa pengembang untuk melanggar beberapa prinsip utama desain berorientasi objek.
Inilah jawaban yang lebih panjang; pertama, pertimbangkan struktur kelas berikut di alam semesta alternatif tempat C # tidak mengizinkan anggota bersembunyi:
Kami ingin menghapus komentar anggota di Bar, dan dengan melakukan itu, izinkan Bar untuk memberikan MyFooString yang berbeda. Namun, kami tidak dapat melakukannya karena akan melanggar larangan realitas alternatif pada persembunyian anggota. Contoh khusus ini akan penuh dengan bug dan merupakan contoh utama mengapa Anda mungkin ingin melarangnya; misalnya, output konsol apa yang akan Anda dapatkan jika Anda melakukan yang berikut?
Dari atas kepala saya, saya sebenarnya tidak yakin apakah Anda akan mendapatkan "Foo" atau "Bar" di baris terakhir itu. Anda pasti akan mendapatkan "Foo" untuk baris pertama dan "Bar" untuk yang kedua, meskipun ketiga variabel referensi contoh yang sama persis dengan keadaan yang sama persis.
Jadi, perancang bahasa, di alam semesta alternatif kita, mencegah kode yang jelas-jelas buruk ini dengan mencegah persembunyian properti. Sekarang, Anda sebagai pembuat kode harus benar-benar melakukan ini. Bagaimana Anda mengatasi batasan itu? Salah satu caranya adalah memberi nama properti Bar secara berbeda:
Sangat legal, tetapi itu bukan perilaku yang kita inginkan. Sebuah instance dari Bar akan selalu menghasilkan "Foo" untuk properti MyFooString, ketika kami menginginkannya untuk menghasilkan "Bar". Kita tidak hanya harus tahu bahwa IFoo kita secara khusus adalah Bar, kita juga harus tahu untuk menggunakan pengakses yang berbeda.
Kami juga bisa, cukup masuk akal, melupakan hubungan orangtua-anak dan mengimplementasikan antarmuka secara langsung:
Untuk contoh sederhana ini, ini adalah jawaban yang sempurna, selama Anda hanya peduli bahwa Foo dan Bar sama-sama IFOO. Kode penggunaan beberapa contoh akan gagal dikompilasi karena Bar bukan Foo dan tidak dapat ditetapkan seperti itu. Namun, jika Foo memiliki beberapa metode yang berguna "FooMethod" yang dibutuhkan Bar, sekarang Anda tidak dapat mewarisi metode itu; Anda harus mengkloning kode di Bar, atau menjadi kreatif:
Ini adalah peretasan yang jelas, dan sementara beberapa implementasi spesifikasi bahasa OO berjumlah sedikit lebih banyak dari ini, secara konseptual itu salah; jika konsumen dari Bar kebutuhan untuk mengekspos fungsi Foo, Bar harus menjadi Foo, tidak memiliki Foo.
Jelas, jika kita mengendalikan Foo, kita bisa membuatnya virtual, lalu menimpanya. Ini adalah praktik konseptual terbaik di alam semesta kita saat ini ketika seorang anggota diperkirakan akan ditimpa, dan akan bertahan di alam semesta alternatif apa pun yang tidak memungkinkan bersembunyi:
Masalah dengan ini adalah bahwa akses anggota virtual, di bawah tenda, relatif lebih mahal untuk dilakukan, dan Anda biasanya hanya ingin melakukannya ketika Anda perlu. Kurangnya persembunyian, bagaimanapun, memaksa Anda untuk menjadi pesimis tentang anggota yang pembuat kode lain yang tidak mengontrol kode sumber Anda mungkin ingin menerapkan kembali; "praktik terbaik" untuk kelas yang tidak tersegel adalah membuat semuanya virtual kecuali Anda tidak menginginkannya secara khusus. Itu juga masih tidak memberi Anda perilaku persembunyian yang tepat; string akan selalu menjadi "Bar" jika instance adalah Bar. Kadang-kadang benar-benar bermanfaat untuk memanfaatkan lapisan data keadaan tersembunyi, berdasarkan tingkat warisan tempat Anda bekerja.
Singkatnya, membiarkan persembunyian anggota adalah kejahatan yang lebih kecil. Tidak memilikinya pada umumnya akan mengarah pada kekejaman yang lebih buruk yang dilakukan terhadap prinsip-prinsip berorientasi objek daripada membiarkannya.
sumber
IEnumerable
danIEnumerable<T>
antarmuka, dijelaskan dalam Eric Libbert ini posting blog pada topik.Jujur, Eric Lippert, pengembang utama di tim C # compiler, menjelaskannya dengan cukup baik (terima kasih Lescai Ionel untuk tautannya). .NET
IEnumerable
danIEnumerable<T>
antarmuka adalah contoh yang baik ketika anggota bersembunyi berguna.Pada hari-hari awal. NET, kami tidak memiliki obat generik. Jadi
IEnumerable
antarmuka terlihat seperti ini:Antarmuka inilah yang memungkinkan kami untuk mengambil
foreach
lebih dari sekumpulan objek, namun kami harus membuang semua objek tersebut agar dapat menggunakannya dengan benar.Lalu datang obat generik. Ketika kami mendapat obat generik, kami juga mendapat antarmuka baru:
Sekarang kita tidak perlu membuang benda saat kita mengulanginya! Woot! Sekarang, jika persembunyian anggota tidak diizinkan, antarmuka harus terlihat seperti ini:
Ini akan agak konyol, karena
GetEnumerator()
danGetEnumeratorGeneric()
dalam kedua kasus melakukan hal yang persis sama , tetapi mereka memiliki nilai pengembalian yang sedikit berbeda. Mereka sangat mirip, pada kenyataannya, bahwa Anda hampir selalu ingin default ke bentuk generikGetEnumerator
, kecuali jika Anda bekerja dengan kode warisan yang ditulis sebelum obat generik diperkenalkan ke .NET.Kadang-kadang anggota bersembunyi tidak memungkinkan lebih banyak ruang untuk kode jahat dan bug sulit-untuk-menemukan. Namun terkadang ini berguna, seperti ketika Anda ingin mengubah jenis pengembalian tanpa melanggar kode lawas. Itu hanya salah satu dari keputusan yang harus dibuat oleh perancang bahasa: Apakah kita merepotkan pengembang yang secara sah membutuhkan fitur ini dan meninggalkannya, atau apakah kita memasukkan fitur ini ke dalam bahasa dan menangkap kritik dari mereka yang menjadi korban penyalahgunaannya?
sumber
IEnumerable<T>.GetEnumerator()
menyembunyikan ituIEnumerable.GetEnumerator()
, ini hanya karena C # tidak memiliki tipe kovarian kembali ketika menimpa. Logikanya itu menimpa, sepenuhnya sejalan dengan LSP. Menyembunyikan adalah ketika Anda memiliki variabel lokalmap
dalam fungsi dalam file yang melakukanusing namespace std
(dalam C ++).Pertanyaan Anda dapat dibaca dua cara: apakah Anda bertanya tentang ruang lingkup variabel / fungsi secara umum, atau Anda mengajukan pertanyaan yang lebih spesifik tentang ruang lingkup dalam hierarki warisan. Anda tidak menyebutkan pewarisan secara khusus, tetapi Anda menyebutkan sulit menemukan bug, yang kedengarannya lebih mirip lingkup dalam konteks pewarisan daripada lingkup biasa, jadi saya akan menjawab kedua pertanyaan.
Lingkup secara umum adalah ide yang baik, karena memungkinkan kita untuk memfokuskan perhatian kita pada satu bagian tertentu (mudah-mudahan kecil) dari program. Karena itu memungkinkan nama lokal selalu menang, jika Anda hanya membaca bagian dari program yang ada dalam cakupan tertentu, maka Anda tahu persis bagian mana yang didefinisikan secara lokal dan apa yang didefinisikan di tempat lain. Entah namanya merujuk ke sesuatu yang lokal, dalam hal ini kode yang mendefinisikannya tepat di depan Anda, atau itu merujuk pada sesuatu di luar lingkup lokal. Jika tidak ada referensi non-lokal yang dapat berubah dari bawah kami (terutama variabel global, yang dapat diubah dari mana saja), maka kami dapat mengevaluasi apakah bagian dari program dalam lingkup lokal sudah benar atau tidak tanpa merujuk ke bagian mana pun dari sisa program sama sekali .
Ini mungkin kadang-kadang menyebabkan beberapa bug, tetapi lebih dari kompensasi dengan mencegah sejumlah besar bug yang mungkin terjadi. Selain membuat definisi lokal dengan nama yang sama dengan fungsi perpustakaan (jangan lakukan itu), saya tidak bisa melihat cara mudah untuk memperkenalkan bug dengan cakupan lokal, tetapi cakupan lokal adalah apa yang memungkinkan banyak bagian dari penggunaan program yang sama i sebagai penghitung indeks untuk satu lingkaran tanpa saling bertabrakan, dan membiarkan Fred menyusuri aula menulis fungsi yang menggunakan string bernama str yang tidak akan mengalahkan string Anda dengan nama yang sama.
Saya menemukan artikel yang menarik dari Bertrand Meyer yang membahas kelebihan muatan dalam konteks warisan. Dia mengemukakan perbedaan yang menarik, antara apa yang dia sebut sintaksis berlebihan (artinya ada dua hal berbeda dengan nama yang sama) dan semantik berlebihan (artinya ada dua implementasi berbeda dari ide abstrak yang sama). Overloading semantik akan baik-baik saja, karena Anda bermaksud mengimplementasikannya secara berbeda dalam subkelas; sintaksis yang berlebihan akan menjadi tabrakan nama yang tidak disengaja yang menyebabkan bug.
Perbedaan antara kelebihan beban dalam situasi pewarisan yang dimaksudkan dan yang merupakan bug adalah semantik (artinya), sehingga kompiler tidak memiliki cara untuk mengetahui apakah yang Anda lakukan benar atau salah. Dalam situasi ruang lingkup yang sederhana, jawaban yang benar selalu merupakan hal lokal, sehingga kompiler dapat mengetahui apa hal yang benar.
Saran Bertrand Meyer adalah menggunakan bahasa seperti Eiffel, yang tidak memungkinkan bentrokan nama seperti ini dan memaksa programmer untuk mengganti nama satu atau keduanya, sehingga menghindari masalah sepenuhnya. Saran saya adalah untuk menghindari penggunaan warisan sepenuhnya, juga menghindari masalah sepenuhnya. Jika Anda tidak dapat atau tidak ingin melakukan salah satu dari hal-hal itu, masih ada hal-hal yang dapat Anda lakukan untuk mengurangi kemungkinan memiliki masalah dengan warisan: ikuti LSP (Prinsip Pergantian Liskov), lebih suka komposisi daripada warisan, tetap hierarki warisan Anda dangkal, dan pertahankan kelas dalam hierarki warisan kecil. Juga, beberapa bahasa mungkin dapat mengeluarkan peringatan, bahkan jika mereka tidak akan mengeluarkan kesalahan, seperti bahasa seperti Eiffel.
sumber
Ini dua sen saya.
Program dapat disusun menjadi blok (fungsi, prosedur) yang merupakan unit mandiri dari logika program. Setiap blok dapat merujuk ke "hal-hal" (variabel, fungsi, prosedur) menggunakan nama / pengidentifikasi. Pemetaan ini dari nama ke benda disebut mengikat .
Nama-nama yang digunakan oleh sebuah blok terbagi dalam tiga kategori:
Pertimbangkan misalnya program C berikut
Fungsi ini
print_double_int
memiliki nama lokal (variabel lokal)d
, dan argumenn
, dan menggunakan eksternal, nama globalprintf
, yang dalam lingkup tetapi tidak didefinisikan secara lokal.Pemberitahuan yang
printf
juga bisa disampaikan sebagai argumen:Biasanya, argumen digunakan untuk menentukan parameter input / output dari suatu fungsi (prosedur, blok), sedangkan nama global digunakan untuk merujuk pada hal-hal seperti fungsi perpustakaan yang "ada di lingkungan", dan oleh karena itu lebih mudah untuk menyebutkannya hanya ketika mereka dibutuhkan. Menggunakan argumen alih-alih nama global adalah ide utama injeksi ketergantungan , yang digunakan ketika dependensi harus dibuat eksplisit alih-alih diselesaikan dengan melihat konteksnya.
Penggunaan serupa lainnya dari nama yang didefinisikan secara eksternal dapat ditemukan di penutupan. Dalam hal ini nama yang didefinisikan dalam konteks leksikal suatu blok dapat digunakan di dalam blok tersebut, dan nilai yang terikat pada nama itu akan (biasanya) terus ada selama blok itu merujuk padanya.
Ambil contoh kode Scala ini:
Nilai kembalinya fungsi
createMultiplier
adalah closure(m: Int) => m * n
, yang berisi argumenm
dan nama eksternaln
. Naman
diselesaikan dengan melihat konteks di mana penutupan didefinisikan: nama terikat dengan argumenn
fungsicreateMultiplier
. Perhatikan bahwa pengikatan ini dibuat ketika penutupan dibuat, yaitu ketikacreateMultiplier
dipanggil. Jadi naman
terikat dengan nilai aktual dari argumen untuk doa fungsi tertentu. Bandingkan ini dengan kasus fungsi perpustakaan sepertiprintf
, yang diselesaikan oleh linker ketika program dieksekusi dibangun.Meringkas, mungkin berguna untuk merujuk nama eksternal di dalam blok kode lokal sehingga Anda
Membayangi masuk ketika Anda menganggap bahwa di blok Anda hanya tertarik pada nama yang relevan yang didefinisikan di lingkungan, misalnya dalam
printf
fungsi yang ingin Anda gunakan. Jika kebetulan Anda ingin menggunakan nama lokal (getc
,putc
,scanf
, ...) yang telah digunakan di lingkungan, Anda menginginkan sederhana untuk mengabaikan (shadow) nama global. Jadi, ketika berpikir secara lokal, Anda tidak ingin mempertimbangkan konteks keseluruhan (mungkin sangat besar).Di arah lain, ketika berpikir secara global, Anda ingin mengabaikan detail internal konteks lokal (enkapsulasi). Karena itu Anda perlu membayangi, jika tidak menambahkan nama global dapat mematahkan setiap blok lokal yang sudah menggunakan nama itu.
Intinya, jika Anda ingin blok kode merujuk ke binding yang ditentukan secara eksternal, Anda perlu membayangi untuk melindungi nama lokal dari yang global.
sumber