Dalam sebagian besar bahasa OOP, objek umumnya dapat berubah dengan set pengecualian terbatas (seperti misalnya tupel dan string dalam python). Dalam sebagian besar bahasa fungsional, data tidak dapat diubah.
Baik objek yang dapat berubah maupun yang tidak berubah membawa daftar kelebihan dan kekurangan mereka sendiri.
Ada bahasa yang mencoba mengawinkan kedua konsep seperti misalnya scala di mana Anda memiliki (secara eksplisit dinyatakan) data yang dapat berubah dan tidak dapat diubah (perbaiki saya jika saya salah, pengetahuan saya tentang scala lebih dari terbatas).
Pertanyaan saya adalah: Apakah kekekalan lengkap (sic!) - yaitu tidak ada objek yang dapat bermutasi begitu telah dibuat - masuk akal dalam konteks OOP?
Apakah ada desain atau implementasi model seperti itu?
Pada dasarnya, apakah kekekalan (lengkap) dan OOP berlawanan atau ortogonal?
Motivasi: Dalam OOP Anda biasanya beroperasi pada data, mengubah (bermutasi) informasi yang mendasarinya, menjaga referensi antara objek-objek tersebut. Misalnya objek kelas Person
dengan anggota yang father
mereferensikan Person
objek lain . Jika Anda mengubah nama ayah, ini segera terlihat oleh objek anak tanpa perlu memperbarui. Menjadi abadi, Anda perlu membuat objek baru untuk ayah dan anak. Tetapi Anda akan memiliki kerfuffle yang jauh lebih sedikit dengan objek bersama, multi-threading, GIL, dll.
sumber
Jawaban:
OOP dan imutabilitas hampir sepenuhnya ortogonal satu sama lain. Namun, pemrograman imperatif dan imutabilitas tidak.
OOP dapat diringkas oleh dua fitur inti:
Enkapsulasi : Saya tidak akan mengakses konten objek secara langsung, melainkan berkomunikasi melalui antarmuka spesifik ("metode") dengan objek ini. Antarmuka ini dapat menyembunyikan data internal dari saya. Secara teknis, ini khusus untuk pemrograman modular daripada OOP. Mengakses data melalui antarmuka yang ditentukan kira-kira setara dengan tipe data abstrak.
Pengiriman Dinamis : Ketika saya memanggil metode pada objek, metode yang dieksekusi akan diselesaikan pada saat dijalankan. (Misalnya dalam OOP berbasis kelas, saya mungkin memanggil
size
metode padaIList
contoh, tetapi panggilan mungkin diselesaikan untuk implementasi diLinkedList
kelas). Pengiriman dinamis adalah salah satu cara untuk memungkinkan perilaku polimorfik.Enkapsulasi kurang masuk akal tanpa mutabilitas (tidak ada keadaan internal yang bisa rusak oleh campur tangan eksternal), tetapi masih cenderung membuat abstraksi lebih mudah bahkan ketika semuanya tidak berubah.
Program imperatif terdiri dari pernyataan yang dieksekusi berurutan. Pernyataan memiliki efek samping seperti mengubah status program. Dengan kekekalan, negara tidak dapat diubah (tentu saja, negara baru dapat dibuat). Oleh karena itu, pemrograman imperatif pada dasarnya tidak kompatibel dengan kekekalan.
Sekarang terjadi bahwa OOP secara historis selalu terhubung dengan pemrograman imperatif (Simula didasarkan pada Algol), dan semua bahasa OOP arus utama memiliki akar imperatif (C ++, Java, C #, ... semuanya berakar pada C). Ini tidak menyiratkan bahwa OOP itu sendiri akan menjadi keharusan atau dapat berubah, ini hanya berarti bahwa implementasi OOP oleh bahasa-bahasa ini memungkinkan mutabilitas.
sumber
Catatan, ada budaya di antara pemrogram berorientasi objek di mana orang menganggap jika Anda melakukan OOP bahwa sebagian besar objek Anda akan bisa berubah, tetapi itu adalah masalah terpisah dari apakah OOP membutuhkan kemampuan berubah - ubah. Juga, budaya itu tampaknya perlahan-lahan berubah ke arah lebih kekekalan, karena paparan orang terhadap pemrograman fungsional.
Scala adalah ilustrasi yang benar-benar bagus bahwa tidak perlu untuk berubah-ubah untuk orientasi objek. Sementara Scala mendukung kemampuan berubah-ubah, penggunaannya tidak disarankan. Scala idiomatik sangat berorientasi pada objek dan juga hampir seluruhnya tidak berubah. Ini sebagian besar memungkinkan mutabilitas untuk kompatibilitas dengan Java, dan karena dalam keadaan tertentu objek tidak stabil tidak berbelit-belit untuk bekerja dengannya.
Bandingkan daftar Scala dan daftar Java , misalnya. Daftar abadi Scala berisi semua metode objek yang sama dengan daftar Java yang bisa berubah-ubah. Lebih lagi, pada kenyataannya, karena Java menggunakan fungsi statis untuk operasi seperti sort , dan Scala menambahkan metode gaya fungsional seperti
map
. Semua ciri khas OOP — enkapsulasi, pewarisan, dan polimorfisme — tersedia dalam bentuk yang akrab bagi pemrogram berorientasi objek dan digunakan dengan tepat.Satu-satunya perbedaan yang akan Anda lihat adalah ketika Anda mengubah daftar, Anda mendapatkan objek baru sebagai hasilnya. Itu sering mengharuskan Anda untuk menggunakan pola desain yang berbeda dari yang Anda bisa dengan objek yang bisa berubah, tetapi tidak mengharuskan Anda untuk meninggalkan OOP sama sekali.
sumber
Ketidakmampuan dapat disimulasikan dalam bahasa OOP, dengan hanya memperlihatkan titik akses objek sebagai metode atau properti hanya baca yang tidak bermutasi data. Kekekalan bekerja sama dalam bahasa OOP seperti halnya dalam bahasa fungsional apa pun, kecuali bahwa Anda mungkin kehilangan beberapa fitur bahasa fungsional.
Anggapan Anda tampaknya bahwa sifat berubah-ubah adalah fitur inti dari orientasi objek. Tetapi mutabilitas hanyalah properti dari objek atau nilai. Orientasi objek mencakup sejumlah konsep intrinsik (enkapsulasi, polimorfisme, pewarisan, dll.) Yang hanya sedikit atau tidak ada hubungannya dengan mutasi, dan Anda masih akan memperoleh manfaat dari fitur-fitur tersebut, bahkan jika Anda membuat semuanya tidak dapat diubah.
Tidak semua bahasa fungsional juga membutuhkan kekekalan. Clojure memiliki anotasi khusus yang memungkinkan jenis bisa berubah, dan sebagian besar bahasa fungsional "praktis" memiliki cara untuk menentukan jenis yang bisa berubah.
Sebuah pertanyaan yang lebih baik untuk ditanyakan mungkin adalah "Apakah kekekalan total masuk akal dalam pemrograman imperatif ?" Saya akan mengatakan jawaban yang jelas untuk pertanyaan itu adalah tidak. Untuk mencapai kekekalan penuh dalam pemrograman imperatif, Anda harus melepaskan hal-hal seperti
for
loop (karena Anda harus mengubah variabel loop) demi rekursi, dan sekarang Anda pada dasarnya pemrograman dalam cara yang fungsional pula.sumber
Seringkali berguna untuk mengkategorikan objek sebagai enkapsulasi nilai atau entitas, dengan perbedaan adalah bahwa jika sesuatu adalah nilai, kode yang menyimpan referensi padanya tidak boleh melihat statusnya berubah dengan cara apa pun yang tidak diawali oleh kode itu sendiri. Sebaliknya, kode yang menyimpan referensi ke suatu entitas dapat mengharapkannya berubah dengan cara di luar kendali pemegang referensi.
Meskipun dimungkinkan untuk menggunakan nilai enkapsulasi menggunakan objek dari tipe yang bisa berubah atau tidak dapat diubah, sebuah objek hanya dapat berperilaku sebagai nilai jika setidaknya satu dari kondisi berikut ini berlaku:
Tidak ada referensi ke objek yang akan pernah terpapar pada apa pun yang dapat mengubah status yang dienkapsulasi di dalamnya.
Pemegang setidaknya salah satu referensi ke objek mengetahui semua kegunaan yang mungkin ada referensi yang ada.
Karena semua instance dari tipe yang tidak dapat diubah secara otomatis memenuhi persyaratan pertama, menggunakannya sebagai nilai adalah mudah. Memastikan bahwa salah satu persyaratan terpenuhi ketika menggunakan tipe yang bisa berubah, sebaliknya, jauh lebih sulit. Sedangkan referensi ke tipe yang tidak dapat diubah dapat dengan bebas diedarkan sebagai cara untuk mengenkapsulasi keadaan yang dienkapsulasi di dalamnya, melewati keadaan yang tersimpan dalam tipe yang dapat berubah memerlukan baik membangun objek pembungkus yang tidak dapat diubah, atau menyalin keadaan yang dienkapsulasi dengan benda yang dipegang secara pribadi ke objek lain yang merupakan baik yang disediakan oleh atau dibangun untuk penerima data.
Tipe yang tidak dapat diubah bekerja dengan sangat baik untuk melewati nilai, dan seringkali paling tidak dapat digunakan untuk memanipulasinya. Namun, mereka tidak begitu baik dalam menangani entitas. Hal terdekat yang dapat dimiliki seseorang pada suatu entitas dalam suatu sistem dengan tipe yang murni tidak dapat diubah adalah fungsi yang, mengingat keadaan sistem, akan melaporkan bahwa atribut dari beberapa bagiannya, atau menghasilkan instance sistem-keadaan baru yang seperti disediakan satu kecuali untuk beberapa bagian tertentu yang akan berbeda dalam beberapa mode yang dipilih. Lebih lanjut, jika tujuan suatu entitas adalah untuk menghubungkan beberapa kode dengan sesuatu yang ada di dunia nyata, mungkin entitas tersebut tidak mungkin untuk menghindari mengekspos keadaan yang bisa berubah.
Misalnya, jika seseorang menerima beberapa data melalui koneksi TCP, seseorang dapat menghasilkan objek "keadaan dunia" baru yang memasukkan data itu dalam buffernya tanpa memengaruhi referensi apa pun ke "keadaan dunia" lama, tetapi salinan lama dari negara dunia yang tidak termasuk kumpulan data terakhir akan rusak dan tidak boleh digunakan karena mereka tidak lagi cocok dengan keadaan soket TCP dunia nyata.
sumber
Dalam c # beberapa tipe tidak dapat diubah seperti string.
Ini tampaknya lebih jauh menunjukkan bahwa pilihan telah dipertimbangkan dengan kuat.
Pasti itu benar-benar kinerja yang menuntut untuk menggunakan tipe yang tidak dapat diubah jika Anda harus memodifikasi tipe itu ratusan ribu kali. Itulah alasan mengapa disarankan untuk menggunakan
StringBuilder
kelas daripadastring
kelas dalam kasus ini.Saya telah melakukan percobaan dengan profiler dan menggunakan tipe yang tidak bisa diubah benar-benar menuntut CPU dan RAM.
Ini juga intuitif jika Anda mempertimbangkan bahwa untuk memodifikasi hanya satu huruf dalam serangkaian 4000 karakter Anda harus menyalin setiap karakter di area lain dari RAM.
sumber
string
serentak berulang . Untuk hampir semua jenis data / kasus penggunaan, struktur persisten yang efisien dapat (sering sudah) ditemukan. Sebagian besar dari mereka memiliki kinerja yang hampir sama, bahkan jika faktor-faktor konstan kadang-kadang lebih buruk.string
(representasi tradisional). Sebuah "string" (dalam representasi yang saya bicarakan) setelah 1000 modifikasi akan sama seperti string yang baru dibuat (konten modulo); tidak ada struktur data persisten yang berguna atau banyak digunakan menurunkan kualitas setelah operasi X. Fragmentasi memori bukan masalah serius (Anda akan memiliki banyak alokasi, ya, tetapi fragmentasi bukan masalah di pengumpul sampah modern)Keabadian lengkap dari segala sesuatu tidak masuk akal dalam OOP, atau sebagian besar paradigma lain dalam hal ini, karena satu alasan yang sangat besar:
Setiap program yang bermanfaat memiliki efek samping.
Program yang tidak menyebabkan perubahan apa pun, tidak berharga. Anda mungkin bahkan belum menjalankannya, karena efeknya akan sama.
Bahkan jika Anda berpikir Anda tidak mengubah apa pun, dan hanya meringkas daftar angka yang entah bagaimana Anda terima, pertimbangkan bahwa Anda perlu melakukan sesuatu dengan hasilnya - apakah Anda mencetaknya ke output standar, tulis ke file, atau dimanapun. Dan itu melibatkan mutasi buffer dan mengubah keadaan sistem.
Sangat masuk akal untuk membatasi mutabilitas pada bagian-bagian yang harus dapat diubah. Tetapi jika benar-benar tidak ada yang perlu diubah, maka Anda tidak melakukan apa pun yang layak dilakukan.
sumber
Saya pikir itu tergantung pada apakah definisi Anda tentang OOP adalah bahwa ia menggunakan gaya penyampaian pesan.
Fungsi murni tidak harus mengubah apa pun karena mereka mengembalikan nilai yang dapat Anda simpan dalam variabel baru.
Dengan gaya pesan yang lewat, Anda memberi tahu objek untuk menyimpan data baru alih-alih menanyakannya data baru apa yang harus Anda simpan dalam variabel baru.
Dimungkinkan untuk memiliki objek dan tidak bermutasi, dengan membuat metodenya fungsi murni yang kebetulan hidup di bagian dalam objek, bukan di luar.
Tetapi tidak mungkin untuk mencampur gaya lewat pesan dan objek yang tidak dapat diubah.
sumber