Bisakah kita benar-benar menggunakan imutabilitas dalam OOP tanpa kehilangan semua fitur OOP utama?

11

Saya melihat manfaat membuat objek dalam program saya tidak berubah. Ketika saya benar-benar berpikir mendalam tentang desain yang baik untuk aplikasi saya, saya sering secara alami tiba di banyak objek saya menjadi tidak berubah. Sering sampai pada titik di mana saya ingin semua benda saya berubah.

Pertanyaan ini berkaitan dengan ide yang sama tetapi tidak ada jawaban yang menunjukkan apa pendekatan yang baik untuk ketetapan dan kapan benar-benar menggunakannya. Apakah ada beberapa pola desain yang tidak berubah? Gagasan umum tampaknya adalah "membuat objek tidak berubah kecuali Anda benar-benar membutuhkannya untuk berubah" yang tidak berguna dalam praktiknya.

Pengalaman saya adalah bahwa ketidakmampuan mendorong kode saya semakin banyak ke paradigma fungsional dan perkembangan ini selalu terjadi:

  1. Saya mulai membutuhkan struktur data yang persisten (dalam arti fungsional) seperti daftar, peta, dll.
  2. Sangat tidak nyaman untuk bekerja dengan referensi silang (mis. Simpul pohon mereferensikan anak-anaknya sementara anak-anak mereferensikan orang tua mereka) yang membuat saya tidak menggunakan referensi silang sama sekali, yang sekali lagi membuat struktur data dan kode saya lebih fungsional.
  3. Warisan berhenti masuk akal dan saya mulai menggunakan komposisi sebagai gantinya.
  4. Seluruh ide dasar OOP seperti enkapsulasi mulai berantakan dan objek saya mulai terlihat seperti fungsi.

Pada titik ini saya praktis tidak menggunakan apa pun dari paradigma OOP lagi dan hanya bisa beralih ke bahasa yang murni fungsional. Jadi pertanyaan saya: apakah ada pendekatan yang konsisten untuk desain OOP abadi yang baik atau apakah selalu jika Anda mengambil ide abadi ke potensi maksimalnya, Anda selalu berakhir pemrograman dalam bahasa fungsional yang tidak memerlukan apa pun dari dunia OOP lagi? Adakah pedoman yang baik untuk memutuskan kelas mana yang harus tidak berubah dan mana yang harus tetap bisa berubah untuk memastikan bahwa OOP tidak berantakan?

Hanya untuk kenyamanan, saya akan memberikan contoh. Mari kita memiliki ChessBoardkoleksi bidak catur abadi (memperluas kelas abstrak)Piece). Dari sudut pandang OOP, sepotong bertanggung jawab untuk menghasilkan gerakan yang valid dari posisinya di papan tulis. Tetapi untuk menghasilkan gerakan potongan membutuhkan referensi ke papannya sementara papan harus memiliki referensi ke potongan-potongannya. Nah, ada beberapa trik untuk membuat referensi silang yang tidak dapat diubah ini tergantung pada bahasa OOP Anda tetapi mereka sulit untuk dikelola, lebih baik tidak memiliki bagian untuk referensi papannya. Tapi kemudian potongan itu tidak dapat menghasilkan gerakannya karena ia tidak tahu keadaan papan. Kemudian potongan menjadi hanya struktur data yang memegang jenis potongan dan posisinya. Anda kemudian dapat menggunakan fungsi polimorfik untuk menghasilkan gerakan untuk semua jenis karya. Ini sangat mungkin dicapai dalam pemrograman fungsional tetapi hampir mustahil di OOP tanpa pemeriksaan jenis runtime dan praktik OOP buruk lainnya ... Lalu,

lishaak
sumber
3
Saya suka pertanyaan dasar, tetapi saya kesulitan dengan detailnya. Misalnya mengapa pewarisan berhenti masuk akal ketika Anda memaksimalkan kekekalan?
Martin Ba
1
Objek Anda tidak memiliki metode?
Berhentilah melukai Monica
4
"Dari sudut pandang OOP, sepotong bertanggung jawab untuk menghasilkan gerakan yang valid dari posisinya di papan tulis." - Jelas tidak, mendesain bagian seperti itu kemungkinan besar akan melukai SRP.
Doc Brown
1
@lishaak Anda "berjuang untuk menjelaskan masalah warisan secara sederhana" karena itu bukan masalah; desain yang buruk adalah masalah. Warisan adalah inti dari OOP, sine qua non jika Anda mau. Banyak dari apa yang disebut "masalah dengan warisan" sebenarnya masalah dengan bahasa yang tidak memiliki sintaks pengesampingan eksplisit, dan mereka jauh kurang bermasalah dalam bahasa yang dirancang lebih baik. Tanpa metode virtual dan polimorfisme, Anda tidak memiliki OOP; Anda memiliki pemrograman prosedural dengan sintaks objek lucu. Jadi tidak heran Anda tidak melihat manfaat OO jika Anda menghindarinya!
Mason Wheeler

Jawaban:

24

Bisakah kita benar-benar menggunakan imutabilitas dalam OOP tanpa kehilangan semua fitur OOP utama?

Tidak mengerti kenapa tidak. Sudah melakukannya bertahun-tahun sebelum Java 8 tetap berfungsi. Pernah mendengar tentang Strings? Bagus dan abadi sejak awal.

  1. Saya mulai membutuhkan struktur data yang persisten (dalam arti fungsional) seperti daftar, peta, dll.

Telah membutuhkan semua itu juga. Memvalidasi iterator saya karena Anda mengubah koleksi ketika saya membaca itu hanya kasar.

  1. Sangat tidak nyaman untuk bekerja dengan referensi silang (mis. Simpul pohon mereferensikan anak-anaknya sementara anak-anak mereferensikan orang tua mereka) yang membuat saya tidak menggunakan referensi silang sama sekali, yang sekali lagi membuat struktur data dan kode saya lebih fungsional.

Referensi melingkar adalah jenis khusus neraka. Kekekalan tidak akan menyelamatkan Anda darinya.

  1. Warisan berhenti masuk akal dan saya mulai menggunakan komposisi sebagai gantinya.

Nah di sini aku bersamamu, tetapi tidak melihat apa hubungannya dengan kekekalan. Alasan saya suka komposisi bukan karena saya suka pola strategi yang dinamis, itu karena itu memungkinkan saya mengubah tingkat abstraksi saya.

  1. Seluruh ide dasar OOP seperti enkapsulasi mulai berantakan dan objek saya mulai terlihat seperti fungsi.

Saya ngeri memikirkan apa gagasan Anda tentang "OOP like enkapsulasi". Jika itu melibatkan getter dan setter maka tolong berhenti memanggil enkapsulasi itu karena tidak. Tidak pernah ada. Ini adalah Pemrograman Berorientasi Aspek manual. Kesempatan untuk memvalidasi dan tempat untuk meletakkan breakpoint bagus tetapi tidak enkapsulasi. Enkapsulasi menjaga hak saya untuk tidak tahu atau peduli apa yang terjadi di dalam.

Objek Anda seharusnya terlihat seperti fungsi. Mereka tas fungsi. Mereka adalah kantong fungsi yang bergerak bersama dan mendefinisikan kembali diri mereka bersama.

Pemrograman fungsional sedang populer saat ini dan orang-orang menumpahkan beberapa kesalahpahaman tentang OOP. Jangan biarkan itu membingungkan Anda untuk percaya ini adalah akhir dari OOP. Fungsional dan OOP dapat hidup bersama dengan cukup baik.

  • Pemrograman fungsional sedang formal tentang tugas.

  • OOP bersikap formal tentang fungsi pointer.

Sungguh itu. Dykstra memberi tahu kami bahwa gotoitu berbahaya sehingga kami mendapat informasi resmi tentang hal itu dan membuat pemrograman terstruktur. Sama seperti itu, kedua paradigma ini adalah tentang menemukan cara untuk menyelesaikan sesuatu sambil menghindari jebakan yang datang dari melakukan hal-hal yang merepotkan ini dengan santai.

Biarkan aku menunjukkanmu sesuatu:

f n (x)

Itu adalah sebuah fungsi. Ini sebenarnya merupakan rangkaian fungsi:

f 1 (x)
f 2 (x)
...
f n (x)

Tebak bagaimana kami mengekspresikannya dalam bahasa OOP?

n.f(x)

Itu sedikit nada memilih implementasi apa fyang digunakan DAN itu memutuskan apa beberapa konstanta yang digunakan dalam fungsi itu (yang terus terang berarti hal yang sama). Sebagai contoh:

f 1 (x) = x + 1
f 2 (x) = x + 2

Itu adalah hal yang sama yang disediakan oleh penutupan. Di mana penutupan mengacu pada ruang lingkupnya, metode objek merujuk ke keadaan instance mereka. Objek dapat melakukan penutupan yang lebih baik. Penutupan adalah fungsi tunggal yang dikembalikan dari fungsi lain. Konstruktor mengembalikan referensi ke seluruh fungsi:

g 1 (x) = x 2 + 1
g 2 (x) = x 2 + 2

Ya, Anda bisa menebaknya:

n.g(x)

f dan g adalah fungsi yang berubah bersama dan bergerak bersama. Jadi kami menempelkannya di tas yang sama. Inilah objek sebenarnya. Memegang nkonstan (tidak berubah) berarti lebih mudah untuk memprediksi apa yang akan dilakukan ketika Anda memanggilnya.

Nah, itu baru strukturnya. Cara saya berpikir tentang OOP adalah banyak hal kecil yang berbicara dengan hal-hal kecil lainnya. Semoga untuk sekelompok kecil pilihan hal-hal kecil. Ketika saya kode saya membayangkan diri saya sebagai objek. Saya melihat sesuatu dari sudut pandang objek. Dan saya mencoba untuk malas jadi saya tidak berlebihan mengerjakan objek. Saya menerima pesan sederhana, mengerjakannya sedikit, dan mengirimkan pesan sederhana hanya kepada teman-teman terbaik saya. Ketika saya selesai dengan objek itu saya melompat ke yang lain dan melihat hal-hal dari perspektif itu.

Kartu Tanggung Jawab Kelas adalah yang pertama mengajarkan saya berpikir seperti itu. Man aku bingung tentang mereka saat itu tetapi sial jika mereka masih relevan hari ini.

Mari kita memiliki ChessBoard sebagai koleksi potongan catur abadi (memperluas Piece kelas abstrak). Dari sudut pandang OOP, sepotong bertanggung jawab untuk menghasilkan gerakan yang valid dari posisinya di papan tulis. Tetapi untuk menghasilkan gerakan potongan membutuhkan referensi ke papannya sementara papan harus memiliki referensi ke potongan-potongannya.

Arg! Lagi dengan referensi lingkaran yang tidak perlu.

Bagaimana dengan: A ChessBoardDataStructuremengubah kabel xy menjadi referensi per satuan. Potongan-potongan itu memiliki metode yang mengambil x, y dan tertentu ChessBoardDataStructuredan mengubahnya menjadi koleksi pukulan merek baru ChessBoardDataStructure. Kemudian dorong itu menjadi sesuatu yang bisa memilih langkah terbaik. Sekarang ChessBoardDataStructurebisa berubah dan potongan juga bisa. Dengan cara ini Anda hanya memiliki satu pion putih di memori. Hanya ada beberapa referensi untuk itu di lokasi yang tepat. Berorientasi objek, fungsional, dan tidak berubah.

Tunggu, bukankah kita sudah bicara tentang catur?

candied_orange
sumber
6
Semua orang harus membaca ini. Dan kemudian membaca On Understanding Data Abstraction, Revisited oleh William R. Cook . Dan kemudian baca ini lagi. Dan kemudian bacalah Proposal Cook untuk Definisi Modern dan "Object" yang disederhanakan dan Modern . Lemparkan dalam Alan Kay's " Ide besar adalah 'pesan' ", definisinya tentang OO
Jörg W Mittag
1
Dan yang terakhir, The Treaty of Orlando .
Jörg W Mittag
1
@ Euphoric: Saya pikir itu adalah formulasi yang cukup standar yang cocok dengan definisi standar untuk "pemrograman fungsional", "pemrograman imperatif", "pemrograman logika", dll. Jika tidak, C adalah bahasa fungsional karena Anda dapat menyandikan FP di dalamnya, bahasa logika, karena Anda dapat menyandikan pemrograman logika di dalamnya, bahasa dinamis, karena Anda dapat menyandikan pengetikan dinamis di dalamnya, bahasa aktor, karena Anda dapat menyandikan sistem aktor di dalamnya, dan sebagainya. Dan Haskell adalah bahasa imperatif terbesar di dunia karena Anda benar-benar dapat menyebutkan efek samping, menyimpannya dalam variabel, memberikannya sebagai ...
Jörg W Mittag
1
Saya pikir kita dapat menggunakan ide-ide serupa dengan yang ada di karya jenius Matthias Felleisen tentang ekspresifitas bahasa. Memindahkan program OO dari, katakanlah, Java ke C♯ dapat dilakukan hanya dengan transformasi lokal, tetapi memindahkannya ke C membutuhkan restrukturisasi global (pada dasarnya, Anda perlu memperkenalkan mekanisme pengiriman pesan dan mengalihkan semua fungsi panggilan melalui itu), oleh karena itu Java dan C♯ dapat mengekspresikan OO dan C dapat "hanya" menyandikannya.
Jörg W Mittag
1
@lishaak Karena Anda menentukannya untuk hal yang berbeda. Ini membuatnya pada satu tingkat abstraksi dan mencegah duplikasi penyimpanan informasi posisi yang sebaliknya bisa menjadi tidak konsisten. Jika Anda hanya membenci pengetikan ekstra maka tempelkan pada suatu metode sehingga Anda hanya mengetiknya sekali ... Sepotong tidak harus mengingat di mana itu jika Anda mengatakan di mana itu. Sekarang potongan hanya menyimpan informasi seperti warna, gerakan, dan gambar / singkatan, yang selalu benar dan tidak berubah.
candied_orange
2

Konsep yang paling berguna diperkenalkan ke dalam arus utama oleh OOP, menurut saya, adalah:

  • Modularisasi yang tepat.
  • Enkapsulasi data.
  • Pemisahan yang jelas antara antarmuka pribadi dan publik.
  • Mekanisme yang jelas untuk ekstensibilitas kode.

Semua manfaat ini juga dapat direalisasikan tanpa rincian implementasi tradisional, seperti warisan atau bahkan kelas. Gagasan asli Alan Kay tentang "sistem berorientasi objek" menggunakan "pesan" alih-alih "metode", dan lebih dekat ke Erlang daripada misalnya C ++. Lihatlah Go yang tidak jauh dengan banyak detail implementasi OOP tradisional, tetapi masih terasa cukup berorientasi objek.

Jika Anda menggunakan objek yang tidak dapat diubah, Anda masih dapat menggunakan sebagian besar barang OOP tradisional: antarmuka, pengiriman dinamis, enkapsulasi. Anda tidak perlu setter, dan seringkali bahkan tidak perlu getter untuk objek yang lebih sederhana. Anda juga dapat memetik manfaat imutabilitas: Anda selalu yakin bahwa suatu objek tidak berubah sementara itu, tidak ada penyalinan defensif, tidak ada balapan data, dan mudah untuk membuat metodenya murni.

Lihatlah bagaimana Scala mencoba untuk menggabungkan pendekatan immutability dan FP dengan OOP. Memang itu bukan bahasa yang paling sederhana dan elegan. Meskipun demikian, ini cukup berhasil secara praktis. Juga, lihat Kotlin yang menyediakan banyak alat dan pendekatan untuk campuran serupa.

Masalah biasa dengan mencoba pendekatan yang berbeda dari pencipta bahasa dalam pikiran N tahun yang lalu adalah 'ketidakcocokan impedansi' dengan perpustakaan standar. OTOH, ekosistem Java dan .NET saat ini memiliki dukungan perpustakaan standar yang wajar untuk struktur data yang tidak dapat diubah; tentu saja, perpustakaan pihak ketiga juga ada.

9000
sumber