Keabadian lengkap dan Pemrograman Berorientasi Objek

43

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 Persondengan anggota yang fathermereferensikan Personobjek 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.

Hyperboreus
sumber
2
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.
Robert Harvey
5
Mutabilitas bukanlah properti bahasa OOP seperti C # dan Java, juga tidak dapat diubah. Anda menentukan kemampuan berubah atau tidak berubah dengan cara Anda menulis kelas.
Robert Harvey
9
Anggapan Anda tampaknya bahwa sifat berubah-ubah adalah fitur inti dari orientasi objek. Bukan itu. Mutabilitas hanyalah sifat benda atau nilai. Orientasi objek meliputi 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 berubah.
Robert Harvey
2
@MichaelT Pertanyaannya bukan tentang membuat hal-hal tertentu bisa berubah, ini tentang membuat semua hal berubah.

Jawaban:

43

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 sizemetode pada IListcontoh, tetapi panggilan mungkin diselesaikan untuk implementasi di LinkedListkelas). 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.

amon
sumber
2
Terima kasih banyak, terutama untuk definisi dari dua fitur inti.
Hyperboreus
Pengiriman dinamis bukan fitur inti dari OOP. Enkapsulasi benar-benar (seperti yang sudah Anda akui).
Stop Harming Monica
5
@OrangeDog Ya, tidak ada definisi OOP yang diterima secara universal, tapi saya perlu definisi untuk bekerja dengannya. Jadi saya memilih sesuatu yang sedekat mungkin dengan kebenaran yang saya dapat tanpa menulis disertasi lengkap tentang itu. Namun, saya menganggap pengiriman dinamis sebagai fitur utama membedakan OOP dari paradigma lain. Sesuatu yang terlihat seperti OOP tetapi sebenarnya semua panggilan diselesaikan secara statis benar-benar hanya pemrograman modular dengan polimorfisme ad-hoc. Objek adalah sepasang metode dan data, dan setara dengan penutupan.
amon
2
"Objek adalah pasangan metode dan data" yang akan dilakukan. Yang Anda butuhkan adalah sesuatu yang tidak ada hubungannya dengan kekekalan.
Stop Harming Monica
3
@CodeYogi Penyembunyian data adalah jenis enkapsulasi yang paling umum. Namun, cara data disimpan secara internal oleh suatu objek bukan satu-satunya detail implementasi yang harus disembunyikan. Sama pentingnya untuk menyembunyikan bagaimana antarmuka publik diimplementasikan, misalnya apakah saya menggunakan metode pembantu. Metode penolong semacam itu juga harus bersifat pribadi, secara umum. Jadi untuk meringkas: enkapsulasi adalah prinsip, sedangkan menyembunyikan data adalah teknik enkapsulasi.
Amon
25

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.

Karl Bielefeldt
sumber
17

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 forloop (karena Anda harus mengubah variabel loop) demi rekursi, dan sekarang Anda pada dasarnya pemrograman dalam cara yang fungsional pula.

Robert Harvey
sumber
Terima kasih. Bisakah Anda menjelaskan sedikit paragraf terakhir Anda ("jelas" mungkin sedikit subyektif).
Hyperboreus
Sudah melakukannya ....
Robert Harvey
1
@ Hyperboreus Ada banyak cara untuk mencapai polimorfisme. Subtipe dengan pengiriman dinamis, polimorfisme ad-hoc statis (alias. Overloading fungsi) dan polimorfisme parametrik (alias generik) adalah cara yang paling umum untuk melakukan itu, dan semua cara memiliki kekuatan dan kelemahannya. Bahasa OOP modern menggabungkan ketiga cara ini, sedangkan Haskell terutama mengandalkan polimorfisme parametrik dan polimorfisme ad-hoc.
amon
3
@RobertHarvey Anda mengatakan bahwa Anda perlu dapat berubah-ubah karena Anda perlu mengulang (jika tidak, Anda harus menggunakan rekursi). Sebelum saya mulai menggunakan Haskell dua tahun lalu, saya pikir saya juga perlu variabel yang bisa berubah. Saya hanya mengatakan ada cara lain untuk "loop" (peta, lipat, filter, dll.). Setelah Anda mengambil perulangan dari tabel, mengapa lagi Anda membutuhkan variabel yang bisa berubah?
cimmanon
1
@RobertHarvey Tapi ini justru inti dari bahasa pemrograman: Apa yang terpapar pada Anda dan bukan apa yang terjadi di bawah tenda. Yang terakhir adalah responsability dari kompiler atau interpreter, bukan dari pengembang aplikasi. Kalau tidak kembali ke assembler.
Hyperboreus
5

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:

  1. Tidak ada referensi ke objek yang akan pernah terpapar pada apa pun yang dapat mengubah status yang dienkapsulasi di dalamnya.

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

supercat
sumber
4

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 StringBuilderkelas daripada stringkelas 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.

Revious
sumber
6
Sering memodifikasi data yang tidak dapat diubah tidak harus lambat seperti stringserentak 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.
@delnan Saya juga berpikir bahwa paragraf terakhir dari jawaban lebih tentang detail implementasi daripada tentang (im) mutabilitas.
Hyperboreus
@ Hiperboreus: apakah Anda pikir saya harus menghapus bagian itu? Tetapi bagaimana sebuah string dapat berubah jika tidak berubah? Maksud saya .. menurut saya, tapi yang pasti saya bisa salah, itu bisa menjadi alasan utama mengapa objek tidak berubah.
Revious
1
@Revious Tidak berarti. Biarkan saja, sehingga menyebabkan diskusi dan opini dan sudut pandang yang lebih menarik.
Hyperboreus
1
@Revious Ya, membaca akan lebih lambat, meskipun tidak selambat mengubah 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)
0

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.

cao
sumber
4
Saya gagal melihat bagaimana jawaban Anda terkait dengan pertanyaan karena saya tidak membahas bahasa fungsional murni. Ambil erlang misalnya: Data tidak dapat diubah, tidak ada tugas yang merusak, tidak ada hazzle tentang efek samping. Anda juga memiliki status dalam bahasa fungsional, hanya saja status "mengalir" melalui fungsi-fungsi, tidak seperti fungsi yang beroperasi pada negara. Negara berubah, tetapi tidak bermutasi di tempat, tetapi negara masa depan menggantikan negara saat ini. Kekekalan bukan tentang apakah buffer memori diubah atau tidak, ini tentang apakah mutasi ini terlihat dari luar.
Hyperboreus
Dan negara masa depan menggantikan yang sekarang bagaimana , tepatnya? Dalam program OO, keadaan itu adalah properti dari suatu objek di suatu tempat. Mengganti status memerlukan pengubahan objek (atau menggantinya dengan yang lain, yang membutuhkan perubahan pada objek yang merujuknya (atau menggantinya dengan yang lain, yang ... eh. Anda mengerti maksudnya)). Anda mungkin menemukan beberapa jenis peretasan monadik, di mana setiap tindakan berakhir dengan membuat aplikasi yang sama sekali baru ... tetapi meskipun begitu, kondisi program saat ini harus direkam di suatu tempat.
cHao
7
-1. Ini salah. Anda membingungkan efek samping dengan mutasi, dan walaupun mereka sering diperlakukan sama oleh bahasa fungsional, mereka berbeda. Setiap program yang bermanfaat memiliki efek samping; tidak setiap program yang bermanfaat mengalami mutasi.
Michael Shaw
@Michael: Ketika datang ke OOP, mutasi dan efek samping sangat terkait sehingga mereka tidak dapat dipisahkan secara realistis. Jika Anda tidak memiliki mutasi, maka Anda tidak dapat memiliki efek samping tanpa peretasan dalam jumlah besar.
cHao
-2

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.

var brandNewVariable = pureFunction(foo);

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.

sameOldObject.changeMe(foo);

Dimungkinkan untuk memiliki objek dan tidak bermutasi, dengan membuat metodenya fungsi murni yang kebetulan hidup di bagian dalam objek, bukan di luar.

var brandNewVariable = nonMutatingObject.askMe(foo);

Tetapi tidak mungkin untuk mencampur gaya lewat pesan dan objek yang tidak dapat diubah.

presley
sumber