Mengapa "kopling ketat antara fungsi dan data" buruk?

38

Saya menemukan kutipan ini di " The Joy of Clojure " di hlm. 32, tetapi seseorang mengatakan hal yang sama kepada saya saat makan malam minggu lalu dan saya sudah mendengarnya di tempat lain juga:

[A] Kelemahan dari pemrograman berorientasi objek adalah hubungan erat antara fungsi dan data.

Saya mengerti mengapa kopling yang tidak perlu buruk dalam aplikasi. Juga saya merasa nyaman mengatakan bahwa keadaan yang dapat berubah dan warisan harus dihindari, bahkan dalam Pemrograman Berorientasi Objek. Tapi saya gagal melihat mengapa menempel fungsi di kelas pada dasarnya buruk.

Maksud saya, menambahkan fungsi ke kelas sepertinya menandai email di Gmail, atau menempelkan file di folder. Ini adalah teknik organisasi yang membantu Anda menemukannya lagi. Anda memilih beberapa kriteria, lalu menyatukan semuanya. Sebelum OOP, program kami adalah sekumpulan besar metode dalam file. Maksud saya, Anda harus meletakkan fungsi di suatu tempat. Mengapa tidak mengaturnya?

Jika ini adalah serangan terselubung pada tipe, mengapa mereka tidak mengatakan bahwa membatasi tipe input dan output ke suatu fungsi adalah salah? Saya tidak yakin apakah saya bisa setuju dengan itu, tapi setidaknya saya akrab dengan argumen pro dan con type safety. Bagi saya ini kedengarannya seperti masalah yang terpisah.

Tentu, kadang-kadang orang salah dan menempatkan fungsionalitas di kelas yang salah. Tetapi dibandingkan dengan kesalahan lain, ini tampak seperti ketidaknyamanan yang sangat kecil.

Jadi, Clojure memiliki ruang nama. Bagaimana menempelkan fungsi pada kelas di OOP berbeda dari menempelkan fungsi di ruang nama di Clojure dan mengapa begitu buruk? Ingat, fungsi dalam kelas tidak harus beroperasi hanya pada anggota kelas itu. Lihatlah java.lang.StringBuilder - ini beroperasi pada semua jenis referensi, atau melalui tinju otomatis, pada jenis apa pun.

PS Kutipan ini merujuk pada sebuah buku yang belum saya baca: Pemrograman Multiparadigma di Leda: Timothy Budd, 1995 .

GlenPeterson
sumber
20
Saya percaya penulis tidak mengerti OOP dengan benar dan hanya perlu satu alasan lagi untuk mengatakan Jawa itu buruk dan Clojure baik. / kata
Euforia
6
Metode instan (tidak seperti fungsi gratis atau metode ekstensi) tidak dapat ditambahkan dari modul lain. Ini menjadi lebih dari batasan ketika Anda mempertimbangkan antarmuka yang hanya dapat diimplementasikan dengan metode instance. Anda tidak dapat mendefinisikan antarmuka dan kelas dalam modul yang berbeda dan kemudian menggunakan kode dari modul ketiga untuk mengikat mereka bersama. Pendekatan yang lebih fleksibel, seperti kelas tipe haskell harus dapat melakukan itu.
CodesInChaos
4
@ Euforia Saya percaya penulis memang mengerti, tetapi komunitas Clojure sepertinya suka membuat sedotan OOP dan membakarnya sebagai patung untuk semua kejahatan pemrograman sebelum kami memiliki pengumpulan sampah yang baik, banyak memori, prosesor cepat, dan banyak ruang disk. Saya berharap mereka akan berhenti memukul pada OOP dan menargetkan penyebab sebenarnya: arsitektur Von Neuman, misalnya.
GlenPeterson
4
Kesan saya adalah bahwa kebanyakan kritik terhadap OOP sebenarnya adalah kritik terhadap OOP seperti yang diterapkan di Jawa. Bukan karena itu pria jerami yang disengaja, tetapi karena itulah yang mereka kaitkan dengan OOP. Ada masalah yang hampir sama dengan orang yang mengeluh tentang pengetikan statis. Sebagian besar masalah tidak melekat dalam konsep, tetapi hanya kekurangan dalam implementasi populer dari konsep itu.
CodesInChaos
3
Judul Anda tidak cocok dengan isi pertanyaan Anda. Mudah untuk menjelaskan mengapa penggabungan ketat antara fungsi dan data buruk, tetapi teks Anda menanyakan pertanyaan "Apakah OOP melakukan ini?", "Jika demikian, mengapa?" dan "Apakah ini hal yang buruk?". Sejauh ini, Anda cukup beruntung untuk menerima jawaban yang berhubungan dengan satu atau lebih dari tiga pertanyaan ini dan tidak ada yang menganggap pertanyaan yang lebih sederhana dalam judul.
itsbruce

Jawaban:

34

Secara teori, kopling fungsi-data longgar membuatnya lebih mudah untuk menambahkan lebih banyak fungsi untuk bekerja pada data yang sama. Sisi buruknya adalah membuatnya lebih sulit untuk mengubah struktur data itu sendiri, yang mengapa dalam prakteknya, kode fungsional yang dirancang dengan baik dan kode OOP yang dirancang dengan baik memiliki tingkat penggandaan yang sangat mirip.

Ambil diarahkan grafik asiklik (DAG) sebagai contoh struktur data. Dalam pemrograman fungsional, Anda masih memerlukan beberapa abstraksi untuk menghindari pengulangan, jadi Anda akan membuat modul dengan fungsi untuk menambah dan menghapus node dan edge, menemukan node dapat dijangkau dari node yang diberikan, membuat penyortiran topologi, dll. Fungsi-fungsi tersebut secara efektif erat digabungkan ke data, meskipun kompilator tidak menegakkannya. Anda dapat menambahkan simpul dengan cara yang sulit, tetapi mengapa Anda menginginkannya? Kekompakan dalam satu modul mencegah kopling ketat di seluruh sistem.

Sebaliknya di sisi OOP, fungsi apa pun selain operasi DAG dasar akan dilakukan dalam kelas "tampilan" yang terpisah, dengan objek DAG diteruskan sebagai parameter. Sangat mudah untuk menambahkan sebanyak mungkin tampilan yang Anda inginkan yang beroperasi pada data DAG, menciptakan tingkat decoupling fungsi-data yang sama seperti yang Anda temukan dalam program fungsional. Kompiler tidak akan mencegah Anda menjejalkan semuanya ke dalam satu kelas, tetapi kolega Anda akan melakukannya.

Mengubah paradigma pemrograman tidak mengubah praktik abstraksi, kohesi, dan penggabungan terbaik, melainkan hanya mengubah praktik mana yang dikompilasi oleh kompiler untuk membantu Anda menegakkan. Dalam pemrograman fungsional, ketika Anda menginginkan penggabungan fungsi-data, ini diberlakukan oleh persetujuan tuan-tuan daripada kompiler. Dalam OOP, pemisahan model-view dipaksakan oleh persetujuan tuan-tuan daripada kompiler.

Karl Bielefeldt
sumber
13

Jika Anda tidak tahu, ini sudah mengambil wawasan ini: Konsep berorientasi objek dan penutupan adalah dua sisi dari koin yang sama. Yang mengatakan, apa itu penutupan? Dibutuhkan variabel (s) atau data dari lingkup sekitarnya dan mengikatnya di dalam fungsi, atau dari perspektif OO Anda secara efektif melakukan hal yang sama ketika Anda, misalnya, mengirimkan sesuatu ke konstruktor sehingga nantinya Anda dapat menggunakannya sepotong data dalam fungsi anggota instance itu. Tetapi mengambil hal-hal dari lingkup sekitar bukanlah hal yang baik untuk dilakukan - semakin besar lingkup sekitarnya, semakin jahat untuk melakukan ini (meskipun secara pragmatis, beberapa kejahatan sering diperlukan untuk menyelesaikan pekerjaan). Penggunaan variabel global membawa ini ke ekstrem, di mana fungsi dalam suatu program menggunakan variabel pada lingkup program - benar-benar jahat. Adadeskripsi yang baik di tempat lain tentang mengapa variabel global itu jahat.

Jika Anda mengikuti teknik OO Anda pada dasarnya sudah menerima bahwa setiap modul dalam program Anda akan memiliki tingkat kejahatan minimum tertentu. Jika Anda mengambil pendekatan fungsional untuk pemrograman, Anda bertujuan untuk ideal di mana tidak ada modul dalam program Anda yang mengandung kejahatan penutupan, meskipun Anda mungkin masih memiliki beberapa, tetapi akan jauh lebih sedikit daripada OO.

Itulah kelemahan dari OO - ini mendorong kejahatan semacam ini, menggabungkan data berfungsi dengan membuat penutupan standar (semacam teori pemrograman jendela yang rusak ).

Satu-satunya sisi positifnya adalah, jika Anda tahu Anda akan menggunakan banyak penutupan untuk memulai, OO setidaknya memberi Anda kerangka kerja yang ideal untuk membantu mengatur pendekatan itu sehingga rata-rata programmer dapat memahaminya. Secara khusus variabel yang ditutup lebih eksplisit dalam konstruktor daripada hanya diambil secara implisit dalam penutupan fungsi. Program fungsional yang menggunakan banyak penutupan seringkali lebih samar daripada program OO yang setara, meskipun tidak selalu kurang elegan :)

Benediktus
sumber
8
Kutipan hari ini: "beberapa kejahatan sering diperlukan untuk menyelesaikan pekerjaan"
GlenPeterson
5
Anda belum benar-benar menjelaskan mengapa hal-hal yang Anda sebut jahat itu jahat; Anda hanya menyebut mereka jahat. Jelaskan mengapa mereka jahat, dan Anda mungkin punya jawaban untuk pertanyaan pria itu.
Robert Harvey
2
Paragraf terakhir Anda menyimpan jawabannya. Ini mungkin satu-satunya sisi positifnya, menurut Anda, tetapi itu bukan hal kecil. Kami yang disebut "programmer biasa" sebenarnya menyambut upacara dalam jumlah tertentu, tentu cukup untuk memberi tahu kami apa yang sedang terjadi.
Robert Harvey
Jika OO dan penutupan adalah identik, mengapa begitu banyak bahasa OO gagal memberikan dukungan eksplisit untuk mereka? Halaman wiki C2 yang Anda kutip memiliki lebih banyak pertikaian (dan kurang konsensus) daripada yang normal untuk situs itu.
itsbruce
1
@itsbruce Mereka dibuat sebagian besar tidak perlu. Variabel yang akan "ditutup" bukannya menjadi variabel kelas yang diteruskan ke objek.
Izkata
7

Ini tentang jenis kopling:

Fungsi yang dibangun menjadi objek untuk bekerja pada objek itu tidak dapat digunakan pada jenis objek lainnya.

Di Haskell Anda menulis fungsi untuk bekerja melawan kelas tipe - jadi ada banyak jenis objek yang dapat digunakan fungsi apa pun, asalkan itu adalah jenis kelas yang diberikan yang berfungsi.

Fungsi bebas memungkinkan decoupling seperti itu yang tidak Anda dapatkan ketika Anda fokus pada penulisan fungsi Anda untuk bekerja di dalam tipe A karena Anda tidak dapat menggunakannya jika Anda tidak memiliki instance tipe A, meskipun fungsi tersebut mungkin jika tidak cukup umum untuk digunakan pada instance B tipe atau instance C.

Jimmy Hoffa
sumber
3
Bukankah itu inti dari seluruh antarmuka? Untuk memberikan hal-hal yang memungkinkan tipe B dan tipe C terlihat sama dengan fungsi Anda, sehingga dapat beroperasi pada lebih dari satu tipe?
Random832
2
@ Random832 benar-benar, tetapi mengapa menanamkan fungsi di dalam tipe data jika tidak bekerja dengan tipe data itu? Jawabannya: Ini satu-satunya alasan untuk menanamkan fungsi dalam tipe data. Anda tidak bisa menulis apa pun kecuali kelas statis dan membuat semua fungsi Anda tidak peduli dengan tipe data yang mereka enkapsulasi untuk membuatnya sepenuhnya dipisahkan dari tipe miliknya, tetapi mengapa bahkan repot-repot memasukkannya ke dalam tipe? Pendekatan fungsional mengatakan: Jangan repot-repot, tuliskan fungsi Anda untuk bekerja ke arah antarmuka, dan kemudian tidak ada alasan untuk merangkumnya dengan data Anda.
Jimmy Hoffa
Anda masih harus mengimplementasikan antarmuka.
Random832
2
@ Random832 antarmuka adalah tipe data; mereka tidak membutuhkan fungsi yang dienkapsulasi di dalamnya. Dengan fungsi gratis, semua antarmuka yang perlu dipuji adalah data yang disediakan untuk fungsi yang akan dilawan.
Jimmy Hoffa
2
@ Random832 untuk berhubungan dengan objek dunia nyata seperti yang sangat umum di OO, pikirkan antarmuka sebuah buku: Ini menyajikan informasi (data), itu saja. Anda memiliki fungsi bebas dari turn-page yang bekerja melawan kelas jenis yang memiliki halaman, fungsi ini bekerja terhadap semua jenis buku, surat kabar, gelendong poster di K-Mart, kartu ucapan, surat, apa pun yang dijepit bersama di sudut. Jika Anda menerapkan turn-page sebagai anggota buku, Anda kehilangan semua hal yang dapat Anda gunakan turn-page, sebagai fungsi bebas yang tidak terikat; itu hanya melempar PartyFoulException bir.
Jimmy Hoffa
4

Di Java dan inkarnasi serupa OOP, metode instance (tidak seperti fungsi gratis atau metode ekstensi) tidak dapat ditambahkan dari modul lain.

Ini menjadi lebih dari batasan ketika Anda mempertimbangkan antarmuka yang hanya dapat diimplementasikan dengan metode instance. Anda tidak dapat mendefinisikan antarmuka dan kelas dalam modul yang berbeda dan kemudian menggunakan kode dari modul ketiga untuk mengikat mereka bersama. Pendekatan yang lebih fleksibel, seperti kelas tipe Haskell harus bisa melakukan itu.

CodesInChaos
sumber
Anda dapat melakukannya dengan mudah di Scala. Saya tidak terbiasa dengan Go, tetapi AFAIK dapat Anda lakukan di sana juga. Di Ruby, itu adalah praktik yang cukup umum juga untuk menambahkan metode ke objek setelah fakta untuk membuatnya sesuai dengan beberapa antarmuka. Apa yang Anda gambarkan agak seperti sistem tipe yang dirancang dengan buruk daripada apa pun yang bahkan terkait dengan OO. Sama seperti eksperimen pemikiran: bagaimana jawaban Anda berbeda ketika berbicara tentang Tipe Data Abstrak dan bukan Objek? Saya tidak percaya itu akan membuat perbedaan, yang akan membuktikan bahwa argumen Anda tidak terkait dengan OO.
Jörg W Mittag
1
@ JörgWMittag Saya pikir maksud Anda adalah tipe data aljabar. Dan CodesInChaos, Haskell dengan sangat eksplisit tidak menyarankan apa yang Anda sarankan. Ini disebut contoh yatim piatu dan mengeluarkan peringatan di GHC.
Daniel Gratzer
3
@ JörgWMittag Kesan saya adalah bahwa banyak yang mengkritik OOP mengkritik bentuk OOP yang digunakan di Jawa dan bahasa-bahasa serupa dengan struktur kelas yang kaku dan fokus dalam metode instan. Kesan saya terhadap kutipan itu adalah bahwa ia mengkritik fokus pada metode instan dan tidak benar-benar berlaku untuk rasa OOP lainnya, seperti apa yang digunakan golang.
CodeInChaos
2
@CodesInChaos Maka mungkin mengklarifikasi ini sebagai "OO berbasis kelas statis"
Daniel Gratzer
@ jozefg: Saya berbicara tentang Tipe Data Abstrak. Saya bahkan tidak melihat bagaimana Tipe Data Aljabar jauh relevan dengan diskusi ini.
Jörg W Mittag
3

Orientasi Objek pada dasarnya tentang Abstraksi Data Prosedural (atau Abstraksi Data Fungsional jika Anda menghilangkan efek samping yang merupakan masalah ortogonal). Dalam arti tertentu, Kalkulus Lambda adalah bahasa Berorientasi Objek tertua dan paling murni, karena hanya menyediakan Abstraksi Data Fungsional (karena tidak memiliki konstruksi selain fungsi).

Hanya operasi dari a satu objek yang dapat memeriksa representasi data objek itu. Bahkan objek lain dengan tipe yang sama tidak dapat melakukannya. (Ini adalah perbedaan utama antara Abstraksi Data Berorientasi Objek dan Tipe Data Abstrak: dengan ADT, objek dengan tipe yang sama dapat memeriksa representasi data satu sama lain, hanya representasi objek dari tipe lain yang disembunyikan.)

Ini artinya beberapa objek dengan tipe yang sama mungkin memiliki representasi data yang berbeda. Bahkan objek yang sama mungkin memiliki representasi data yang berbeda pada waktu yang berbeda. (Misalnya, dalam Scala, Maps dan Sets beralih antara array dan hash trie tergantung pada jumlah elemen karena untuk jumlah yang sangat kecil pencarian linear dalam array lebih cepat daripada pencarian logaritma di pohon pencarian karena faktor konstan yang sangat kecil .)

Dari luar objek, Anda seharusnya tidak, Anda tidak bisa tahu representasi datanya. Itu kebalikan dari kopling ketat.

Jörg W Mittag
sumber
Saya memiliki kelas di OOP yang beralih struktur data internal tergantung pada keadaan sehingga contoh objek dari kelas ini dapat menggunakan representasi data yang sangat berbeda pada saat yang sama. Menyembunyikan dan enkapsulasi data dasar menurut saya? Jadi, bagaimana Peta di Scala berbeda dari kelas Peta yang diimplementasikan dengan benar (menyembunyikan data dan enkapsulasi) dalam bahasa OOP?
Marjan Venema
Dalam contoh Anda, mengenkapsulasi data Anda dengan fungsi pengakses dalam kelas (dan dengan demikian menyatukan fungsi-fungsi tersebut dengan data tersebut) sebenarnya memungkinkan Anda untuk secara longgar memasangkan instance dari kelas tersebut dengan seluruh program Anda. Anda menyangkal titik pusat dari kutipan - sangat bagus!
GlenPeterson
2

Kopling ketat antara data dan fungsi buruk karena Anda ingin dapat mengubah satu sama lain secara independen dan kopling ketat membuat ini sulit karena Anda tidak dapat mengubah satu tanpa sepengetahuan dan mungkin mengubah, yang lain.

Anda ingin data yang berbeda disajikan ke fungsi untuk tidak memerlukan perubahan dalam fungsi dan Anda ingin dapat membuat perubahan pada fungsi tanpa memerlukan perubahan apa pun pada data yang dioperasikan untuk mendukung perubahan fungsi tersebut.

Michael Durrant
sumber
1
Ya saya mau itu. Tetapi pengalaman saya adalah bahwa ketika Anda mengirim data ke fungsi non-sepele yang tidak dirancang untuk ditangani secara eksplisit, fungsi itu cenderung rusak. Saya tidak hanya mengacu pada keamanan jenis, tetapi kondisi data apa pun yang tidak diantisipasi oleh penulis (s) fungsi. Jika fungsinya sudah tua dan sering digunakan, perubahan apa pun yang memungkinkan data baru mengalir melaluinya mungkin memecahnya untuk beberapa bentuk data lama yang masih perlu bekerja. Sementara decoupling mungkin ideal untuk fungsi vs data, kenyataan bahwa decoupling bisa sulit dan berbahaya.
GlenPeterson