De ce este greșită "legătura strânsă între funcții și date"?

I found this quote in "The Joy of Clojure" on p. 32, but someone said the same thing to me over dinner last week and I've heard it other places as well:

[A] dezavantajul programării orientate obiect este strâns   cuplarea între funcție și date.

Înțeleg de ce cuplarea inutilă este rea într-o aplicație. De asemenea, sunt convins să spun că starea mutabilă și moștenirea ar trebui evitate, chiar și în programarea orientată pe obiecte. Dar nu reușesc să văd de ce lipirea funcțiilor pe clase este în mod inerent rău.

Adăugarea unei funcții la o clasă pare ca etichetarea unui mesaj în Gmail sau lipirea unui fișier într-un dosar. Este o tehnică organizațională care vă ajută să o găsiți din nou. Alegi câteva criterii, apoi puneți lucrurile împreună. Înainte de PLO, programele noastre erau niște pungi destul de mari de metode în fișiere. Vreau să spun, trebuie să puneți funcții undeva. De ce nu le organizați?

Dacă este vorba despre un atașament închis pe tipuri, de ce nu spun că restricționarea tipului de intrare și ieșire la o funcție este greșită? Nu sunt sigur dacă aș fi de acord cu asta, dar cel puțin sunt familiarizat cu argumentele pro și con. Acest lucru mi se pare o preocupare separată.

Sigur, uneori oamenii se înșeală și pun funcționalitatea în clasa greșită. Dar, în comparație cu alte greșeli, acest lucru pare a fi un inconvenient foarte mic.

Deci, Clojure are spații de nume. Cum se lipsește o funcție pe o clasă în OOP diferită de a lipi o funcție într-un spațiu de nume în Clojure și de ce este atât de rău? Amintiți-vă că funcțiile dintr-o clasă nu operează neapărat doar membrii acelei clase. Uită-te la java.lang.StringBuilder - funcționează pe orice tip de referință, sau prin auto-box, pe orice tip, la toate.

P.S. Acest citat se referă la o carte pe care nu am citit-o: Programare multiparadigma în Leda: Timothy Budd, 1995 .

36
Chiar dacă vă organizați fișierele pline de metode la fel ca și dvs. cu clasele, este mai mult pentru POR decât pentru a putea organiza și a depune codul. VB are module și clase; nu confunda cele două.
adăugat autor bstpierre, sursa
Titlul dvs. nu se potrivește cu corpul întrebării dvs. Este ușor să explici de ce cuplarea strânsă a funcțiilor și a datelor este rău, dar textul dvs. pune întrebările "OOP face acest lucru?", "Dacă da, de ce?" și "Este un lucru rău?". Până acum, ați fost destul de norocoși să primiți răspunsuri care se referă la una sau mai multe dintre aceste trei întrebări și niciunul nu a asumat o întrebare mai simplă în titlu.
adăugat autor Stella Biderman, sursa
Impresia mea este că majoritatea criticilor față de OOP sunt de fapt critici ale OOP implementate în Java. Nu pentru că este un om de paie deliberat, ci pentru că este ceea ce se asociază cu OOP. Există probleme destul de similare cu cei care se plâng de tastarea statică. Cele mai multe dintre aspecte nu sunt inerente conceptului, ci doar defecte într-o implementare populară a acestui concept.
adăugat autor user8669, sursa
@GlenPeterson Pentru că inițial am vrut să scriu doar o singură propoziție și ea a continuat să crească
adăugat autor user8669, sursa
Metodele de instanță (spre deosebire de funcțiile libere sau metodele de extensie) nu pot fi adăugate din alte module. Acest lucru devine mai mult o restricție atunci când luați în considerare interfețele care pot fi implementate numai prin metode de instanță. Nu puteți defini o interfață și o clasă în diferite module și apoi utilizați codul dintr-un al treilea modul pentru a le lega împreună. O abordare mai flexibilă, ca și clasele tipului lui Haskell, ar trebui să poată face acest lucru.
adăugat autor user8669, sursa
Înainte de OOP, programele noastre erau niște pungi de metode destul de mari în fișiere. Vreau să spun, trebuie să puneți funcții undeva. De ce să nu le organizați? - "Puneți funcții similare pe același obiect" este "OOP" în același mod în care C ++ este "C cu clase". Din punctul de vedere al outsider, bine, s-ar putea uite în acest fel, dar nu faci OOP/C ++ într-un mod ușor de întreținut dacă asta criteriile pe care le folosești.
adăugat autor Izkata, sursa
Notă laterală: Puteți să utilizați (și ar trebui) să utilizați structuri de date în OOP dacă doriți decuplarea datelor și a funcțiilor. Codul de procedură (codul care utilizează structurile de date) facilitează adăugarea de noi funcții fără a schimba structurile de date existente. Codul OO, pe de altă parte, facilitează adăugarea de clase noi fără modificarea funcțiilor existente. Programatorii maturi știu când să folosească unul și când să folosească celălalt. Uneori, într-adevăr doriți structuri simple de date cu proceduri care operează pe ele.
adăugat autor WoJ, sursa
@Euphoric Cred că scriitorul a înțeles, dar comunitatea Clojure pare să-i placă să facă un om de paie de la OOP și să-l ardă ca o efigie pentru toate relele programării înainte de a avea o colecție bună de gunoi, o mulțime de memorie, procesoare rapide și o mulțime de spațiu pe disc. Mi-aș fi dorit să renunțe la bătăile pe POR și să vizeze cauzele reale: arhitectura lui Von Neuman, de exemplu.
adăugat autor GlenPeterson, sursa
Cine vorbește despre prostiile astea peste cină? Va duce doar la piraterie groaznice
adăugat autor James, sursa
Cred că scriitorul pur și simplu nu a înțeles corect OOP și tocmai avea nevoie de încă un motiv pentru a spune că Java este rău și Clojure este bun. /declama
adăugat autor Euphoric, sursa
Împreună cu faptul că moștenirea este rea (trebuie să conțină mai degrabă decât să se extindă) și nu există nici un avantaj în înlocuirea condiționalilor cu polimorfismul, putem conchide că OOP are sens de la început, valjok.blogspot.com/2013/01/…
adăugat autor Noah Spurrier, sursa

6 răspunsuri

Teoretic, cuplarea de date funcționale-pierdere facilitează adăugarea mai multor funcții pentru a lucra la aceleași date. În partea inferioară este mai dificil să se schimbe structura de date în sine, de aceea, în practică, un cod funcțional bine conceput și un cod OOP bine conceput au nivele foarte apropiate de cuplare.

Luați un grafic aciclic direct (DAG) ca o structură de date de exemplu. În programarea funcțională aveți nevoie de abstracție pentru a evita repetarea, astfel că veți face un modul cu funcții de adăugare și ștergere a nodurilor și marginilor, găsirea de noduri accesibile dintr-un nod dat, crearea unei sortimente topologice etc. Aceste funcții sunt efectiv strâns legate de date, chiar dacă compilatorul nu o aplică. Puteți adăuga un nod pe calea cea mai grea, dar de ce ați dori? Coerența într-un singur modul împiedică cuplarea strânsă pe întregul sistem.

În schimb, pe partea OOP, orice alte funcții, altele decât operațiile DAG de bază, vor fi realizate în clase separate de vizualizare, cu obiectul DAG transmis ca parametru. Este la fel de ușor să adăugați cât mai multe vizualizări pe care le doriți, care funcționează pe datele DAG, creând același nivel de decuplare a datelor funcționale, așa cum ați afla în programul funcțional. Compilatorul nu te va împiedica să strângi totul într-o clasă, dar colegii tăi vor.

Modificarea paradigmelor de programare nu schimbă cele mai bune practici de abstractizare, coeziune și cuplare, ci doar schimbă practicile pe care compilatorul le ajută să le impuneți. În programarea funcțională, atunci când doriți să cuplați funcția-date este impusă de acordul domnului, mai degrabă decât compilatorul. În PLO, separarea model-vedere este impusă de acordul domnului, mai degrabă decât de compilator.

31
adăugat

În cazul în care nu știați că are deja această înțelegere: Conceptele orientate pe obiecte și închideri sunt două fețe ale aceleiași monede. Acestea fiind spuse, ce este o închidere? Este nevoie de variabila (e) sau de date din domeniul înconjurător și se leagă de ea în interiorul funcției, sau dintr-o perspectivă OO faceți în mod efectiv același lucru atunci când, de exemplu, treceți ceva într-un constructor astfel încât mai târziu să puteți folosi acel o parte dintr-o funcție membră a acelei instanțe. Dar luarea de lucruri din domeniul înconjurător nu este un lucru frumos de făcut - cu cât domeniul de aplicare este mai mare, cu atât este mai rău să faci acest lucru (deși pragmatic, unele rău sunt adesea necesare pentru a obține o muncă). Folosirea variabilelor globale duce acest lucru la extrem, unde funcțiile dintr-un program folosesc variabilele din sfera programului - cu adevărat rău. Există descrieri bune în altă parte despre motivul pentru care variabilele globale sunt rele.

Dacă urmați tehnicile OO, practic acceptați deja că fiecare modul din programul dvs. va avea un anumit nivel minim de rău. Dacă luați o abordare funcțională a programării, sunteți în căutarea unui ideal în care niciun modul din programul dvs. nu va conține închiderea răului, deși este posibil să aveți ceva, dar va fi mult mai puțin decât OO.

Aceasta este dezavantajul OO - încurajează acest tip de rău, cuplarea datelor să funcționeze prin încheierea de standarde de închidere (un fel de teoria ferestrei sparte de programare).

Singurul avantaj este că, dacă știți că veți folosi o mulțime de închideri pentru a începe, OO vă oferă cel puțin un cadru ideologic care să vă ajute să organizați această abordare astfel încât programatorul mediu să o poată înțelege. În special, variabilele care sunt închise sunt explicite în constructor, mai degrabă decât luate implicit într-o închidere a funcției. Programele funcționale care utilizează multe închideri sunt adesea mai criptice decât programul OO echivalent, deși nu neapărat mai puțin elegant :)

13
adăugat
Ultimul tău paragraf salvează răspunsul. Poate că este singura parte plus, în opinia dvs., dar nu este nimic mic. Noi, așa-zișii "programatori obișnuiți", salută cu adevărat o anumită ceremonie, destul de sigur să ne spună ce naiba se întâmplă.
adăugat autor sgwill, sursa
Nu ați explicat cu adevărat de ce lucrurile pe care le numiți rău sunt rele; îi spui doar rău. Explicați de ce sunt răi și ați putea avea un răspuns la întrebarea domnului.
adăugat autor sgwill, sursa
@Izkata Inutil, într-un limbaj sigur pentru tip, cu prețul cantităților mari de plăci de cazan și subclasare nesfârșită (sau logică imperativă și nelistată). Nu aș numi asta "în mare măsură inutil". Este posibil , dar nu aș vrea să merg acolo dacă am o alternativă sănătoasă.
adăugat autor Stella Biderman, sursa
Dacă OO și închiderile sunt sinonime, de ce atât de multe limbi OO nu au oferit sprijin explicit pentru ele? Pagina wiki C2 pe care o citezi are și mai multă dispută (și mai puțin consens) decât este normal pentru site-ul respectiv.
adăugat autor Stella Biderman, sursa
@itsbruce Vezi, "la costul unor cantitati mari de placi de cazane si subclasare nesfarsita (sau logica imperativa si nelocuita)" suna ca OOP facut gresit, ca si cum cineva a zambit cu buzzwords si ce este posibil in OOP, la costul unui cod care poate fi întreținut. Chiar și în Java, boilerplate-ul nu va fi mare dacă se face bine (deși va fi mai mult decât, de exemplu, Python).
adăugat autor Izkata, sursa
@itsbruce Sunt făcute în mare măsură inutile. Variabilele care ar fi "închise peste" devin variabile de clasă transferate în obiect.
adăugat autor Izkata, sursa
De fapt, în Java, închiderea lexicală este uneori singura modalitate simplă de a asigura imuabilitatea - vedeți exemplul "Immutable Map" aici: glenpeterson.blogspot.com/2013/07/…
adăugat autor GlenPeterson, sursa
Vreau să califică comentariul meu anterior. Ar fi trebuit să spun: "Programarea funcțională Citat de zi - un rău este adesea necesar pentru a obține o treabă." Nu susțin răul în general, doar în sensul programării pur funcționale.
adăugat autor GlenPeterson, sursa
Citat al zilei: "unele rău sunt adesea necesare pentru a obține o muncă"
adăugat autor GlenPeterson, sursa
Am adăugat o legătură cu o altă întrebare care vorbește despre răul statului global. Poate că am exercitat o licență poetică atunci când am spus că este "singura parte plus". Cred că ați înțeles greșit ce înțelegeam prin "programator mediu". Adică o ființă umană normală a cărei profesie este programator - și, deși este de așteptat un anumit nivel de competență, e elitist să cerem ca acel nivel să fie cel al unui psihic. Sunt de acord cu tine - orice paradigmă sau limbă care forțează sau încurajează codul lizibil într-un fel merită sărbătorește. Eu însumi, nu sunt întotdeauna convins de abordarea OO, dar eu sunt eu.
adăugat autor Benedict, sursa

Este vorba despre cuplarea tip :

O funcție încorporată într-un obiect pentru a lucra la acel obiect nu poate fi folosit pe alte tipuri de obiecte.

În Haskell scrieți funcții pentru a lucra împotriva tipurilor clase - deci există multe tipuri diferite de obiecte pe care o funcție dată le poate acționa, atâta timp cât este un tip al clasei această funcție funcționează.

Funcțiile independente permit o astfel de decuplare pe care nu o primiți atunci când vă concentrați pe scrierea funcțiilor dvs. pentru a lucra în interiorul tipului A, deoarece atunci nu le puteți utiliza dacă nu aveți o instanță de tip A, chiar dacă funcția ar putea altfel trebuie să fie suficient de general pentru a fi utilizat pe o instanță de tip B sau pe un exemplu de tip C.

7
adăugat
@ Random832 absolut, dar de ce încorpora o funcție în interiorul unui tip de date, dacă nu să lucreze cu acel tip de date? Răspunsul: Este singurul motiv pentru care încorporeze o funcție într-un tip de date. Nu puteai scrie decât clase statice și să-ți faci toate funcțiile nu să îngrijești de tipul de date pe care sunt încapsulate pentru a le face complet decuplate de tipul lor de proprietate, dar apoi de ce să-i pese de a le pune într-un tip ? Abordarea funcțională spune: Nu vă deranjați, nu scrieți funcțiile dvs. pentru a lucra spre interfețe de fel, și atunci nu există nici un motiv să le încapsulați cu datele dvs.
adăugat autor Jimmy Hoffa, sursa
@ Random832 interfețele sunt tipuri de date; ei nu au nevoie de funcții încapsulate în ele. Cu funcții libere, toate interfețele trebuie să extoll este ceea ce datele pe care le pun la dispoziție pentru funcționarea funcțiilor.
adăugat autor Jimmy Hoffa, sursa
@ Random832 să se refere la obiectele din lumea reală așa cum este atât de comun în OO, gândiți-vă la interfața unei cărți: prezintă informații (date), asta-i tot. Aveți funcția gratuită a unei pagini de tip turn-page, care funcționează împotriva claselor de tipuri care au pagini, această funcție funcționează împotriva tuturor tipurilor de cărți, articolelor de știri, acelor axe poster la K-Mart, felicitări, poștă, ceva legat împreună în colţ. Dacă ați implementat pagina de rând ca membru al cărții, vă lipsesc toate lucrurile pe care le-ați putea utiliza în revistă, ca o funcție gratuită, nu este obligatorie; doar aruncă o petrecere pe bere.
adăugat autor Jimmy Hoffa, sursa
Nu este acest punct de interfețe? Pentru a oferi lucrurilor care permit tipului B și tipului C să arate la fel cu funcția dvs., astfel încât acesta să poată funcționa pe mai multe tipuri?
adăugat autor Aaron C. de Bruyn, sursa
Mai trebuie să implementați interfețele.
adăugat autor Aaron C. de Bruyn, sursa
@JimmyHoffa Cred că am înțeles în sfârșit ce este programarea funcțională. Mulțumiri
adăugat autor Mahdi, sursa

În Java și incarnările similare ale OOP, metodele instanței (spre deosebire de funcțiile libere sau metodele de extensie) nu pot fi adăugate din alte module.

Acest lucru devine mai mult o restricție atunci când luați în considerare interfețele care pot fi implementate numai prin metode de instanță. Nu puteți defini o interfață și o clasă în diferite module și apoi utilizați codul dintr-un al treilea modul pentru a le lega împreună. O abordare mai flexibilă, cum ar fi clasele de tip Haskell, ar trebui să poată face acest lucru.

4
adăugat
@jozefg: Vorbesc despre tipurile de date abstracte. Nici măcar nu văd cum sunt tipurile de date algebrice relevante pentru această discuție.
adăugat autor Lawrence B. Crowell, sursa
Puteți face asta ușor în Scala. Nu sunt familiarizat cu Go, dar AFAIK o puteți face și acolo. În Ruby, este o practică destul de obișnuită de a adăuga metode la obiecte după ce le facem să se conformeze unei anumite interfețe. Ceea ce descrieți seamănă mai degrabă cu un sistem de tip rău proiectat decât cu orice altceva legat de OO. Exact ca un experiment de gândire: cum ar fi răspunsul dvs. diferit atunci când vorbim despre tipuri de date abstracte în loc de obiecte? Nu cred că ar face diferența, ceea ce ar dovedi că argumentul tău nu are nicio legătură cu OO.
adăugat autor Lawrence B. Crowell, sursa
@ JörgWMittag Impresia mea este că mulți care critică OOP critică forma OOP folosită în Java și limbi similare cu structura ei rigidă de clasă și se concentrează în metode de exemplu. Impresia mea din acel citat este că critică accentul pe metodele instanței și nu se aplică în realitate altor arome ale OOP, cum ar fi ceea ce utilizează golang.
adăugat autor user8669, sursa
@ JörgWMittag Cred că ați vrut să spuneți tipuri de date algebrice. Și CodesInChaos, Haskell descurajează foarte explicit ceea ce sugerează. Se numește o instanță orfană și emite avertismente asupra GHC.
adăugat autor jozefg, sursa
@CodesInChaos Apoi, poate clarificând acest lucru ca "clasa statică bazată pe OO"
adăugat autor jozefg, sursa
Dacă aveți două module diferite A și B care "leagă împreună" o clasă C și o interfață I, și treci obiectul la o funcție F care se așteaptă la această interfață, atunci cum F știe dacă să folosească A sau B atunci când operează pe C? Singura modalitate este de a folosi un obiect wrapper - chiar dacă nu există două module, singura modalitate pentru ca F să găsească modulul "binding" este să îi transmită un obiect care știe despre el (adică obiectul wrapper)
adăugat autor Aaron C. de Bruyn, sursa

Orientarea obiectelor este fundamentală în ceea ce privește abstractizarea datelor de procedură (sau abstractizarea datelor funcționale dacă eliminați efectele secundare care reprezintă o problemă ortogonală). Într-un sens, calculul lambda este cea mai veche și cea mai pură limbă orientate-obiect, deoarece numai furnizează date Abstracție funcțională (deoarece nu are nici un constructe în afară de funcții).

Doar operațiile unui obiect unic pot inspecta reprezentarea datelor obiectului respectiv. Nici măcar alte obiecte din același tip nu pot face acest lucru. (Aceasta este principala diferență între Abstractizarea de date orientată pe obiecte și Tipurile de date abstracte: cu ADT-uri, obiectele de același tip se pot inspecta reciproc în reprezentarea datelor, doar reprezentarea obiectelor din alte tipuri este ascunsă. )

Ceea ce înseamnă că mai multe obiecte de același tip pot avea reprezentări diferite de date. Chiar același obiect poate avea reprezentări diferite de date la momente diferite. (De exemplu, în Scala, Map s și Set s comuta între un tablou și un hash trie în funcție de numărul de elemente, deoarece pentru numere foarte mici căutarea liniară într- este mai rapid decât căutarea logaritmică într-un arbore de căutare din cauza factorilor constanți foarte mici.)

Din afara unui obiect, nu trebuie, nu cunoașteți reprezentarea datelor. Asta e opusul de cuplare strânsă.

3
adăugat
Am cursuri în OOP care schimba structurile de date interne în funcție de circumstanțe, astfel încât instanțele obiect ale acestor clase pot folosi reprezentări de date foarte diferite în același timp. Decrierea și încapsularea datelor de bază aș spune? Deci, cum este harta în Scala diferită de o clasă Map implementată în mod corespunzător (clasa Wrt ascunderea și încapsularea) într-un limbaj OOP?
adăugat autor Paul Roub, sursa
În exemplul dvs., încapsularea datelor cu funcțiile accessor într-o clasă (și, prin urmare, strâns legarea acelor funcții de acele date) vă permite de fapt să cuplați cu ușurință instanțele acelei clase cu restul programului dvs. Refuzați punctul central al citării - foarte frumos!
adăugat autor GlenPeterson, sursa

Strâns legătura dintre date și funcții este rău pentru că doriți să puteți schimba fiecare independent unul de celălalt, iar cuplarea strânsă face ca acest lucru să fie greu, deoarece nu puteți schimba unul fără cunoașterea și, eventual, schimbări în cealaltă.

Doriți ca diferite date prezentate funcției să nu necesite modificări ale funcției și, în mod similar, doriți să puteți modifica funcția fără a fi nevoie de modificări ale datelor pe care operează pentru a sprijini aceste schimbări de funcții.

1
adăugat
Da vreau aia. Dar experiența mea este că, atunci când trimiteți date unei funcții non-trivială pe care nu a fost concepută în mod explicit să o gestioneze, această funcție tinde să se rupă. Nu mă refer doar la siguranța tipului, ci mai degrabă la orice condiție de date care nu a fost anticipată de autorul (autorii) funcției. Dacă funcția este veche și adesea folosită, orice schimbare care permite fluxul de date noi este probabil să o rupă pentru unele forme vechi de date care trebuie încă să funcționeze. În timp ce decuplarea poate fi ideală pentru funcțiile vs. date, realitatea decuplării poate fi dificilă și periculoasă.
adăugat autor GlenPeterson, sursa