Perché "l'accoppiamento stretto tra funzioni e dati" è negativo?


38

Ho trovato questa citazione in " La gioia del clojure " a p. 32, ma qualcuno mi ha detto la stessa cosa a cena la scorsa settimana e l'ho sentito anche in altri posti:

[A] L'aspetto negativo della programmazione orientata agli oggetti è lo stretto accoppiamento tra funzione e dati.

Capisco perché l'accoppiamento non necessario è negativo in un'applicazione. Inoltre mi sento a mio agio nel dire che lo stato mutevole e l'ereditarietà dovrebbero essere evitati, anche nella programmazione orientata agli oggetti. Ma non riesco a capire perché attaccare le funzioni alle classi sia intrinsecamente negativo.

Voglio dire, aggiungere una funzione a una classe sembra taggare un messaggio in Gmail o incollare un file in una cartella. È una tecnica organizzativa che ti aiuta a ritrovarla. Scegli alcuni criteri, quindi metti insieme cose simili. Prima di OOP, i nostri programmi erano praticamente grandi quantità di metodi nei file. Voglio dire, devi mettere le funzioni da qualche parte. Perché non organizzarli?

Se si tratta di un attacco velato ai tipi, perché non dicono semplicemente che limitare il tipo di input e output a una funzione è sbagliato? Non sono sicuro che potrei essere d'accordo con questo, ma almeno ho familiarità con argomenti di sicurezza pro e contro. Questo mi sembra una preoccupazione per lo più separata.

Certo, a volte le persone sbagliano e mettono la funzionalità nella classe sbagliata. Ma rispetto ad altri errori, questo sembra un piccolo inconveniente.

Quindi, Clojure ha spazi dei nomi. In che modo attaccare una funzione su una classe in OOP è diverso dall'attaccare una funzione in uno spazio dei nomi in Clojure e perché è così male? Ricorda, le funzioni di una classe non operano necessariamente solo sui membri di quella classe. Guarda java.lang.StringBuilder: funziona su qualsiasi tipo di riferimento, o tramite l'auto-boxing, su qualsiasi tipo.

PS Questa citazione fa riferimento a un libro che non ho letto: Programmazione multiparadigm in Leda: Timothy Budd, 1995 .


20
Credo che lo scrittore semplicemente non abbia capito bene OOP e abbia solo bisogno di un motivo in più per dire che Java è cattivo e Clojure è buono. / rant
Euforico

6
I metodi di istanza (diversamente dalle funzioni libere o dai metodi di estensione) non possono essere aggiunti da altri moduli. Questo diventa più una limitazione se si considerano le interfacce che possono essere implementate solo con i metodi di istanza. Non è possibile definire un'interfaccia e una classe in moduli diversi e quindi utilizzare il codice di un terzo modulo per collegarli insieme. Un approccio più flessibile, come le classi di tipi di haskell, dovrebbe essere in grado di farlo.
CodesInChaos,

4
@Euforico Credo che lo scrittore abbia capito, ma la comunità Clojure sembra voler fare un uomo di paglia di OOP e bruciarlo come un'effigie per tutti i mali della programmazione prima che avessimo una buona raccolta dei rifiuti, molta memoria, processori veloci e molto spazio su disco. Vorrei che smettessero di battere su OOP e prendessero di mira le vere cause: l'architettura Von Neuman, per esempio.
GlenPeterson,

4
La mia impressione è che la maggior parte delle critiche a OOP siano in realtà critiche a OOP implementate in Java. Non perché sia ​​un uomo di paglia deliberato, ma perché è ciò che associano a OOP. Ci sono problemi abbastanza simili con le persone che si lamentano della digitazione statica. La maggior parte dei problemi non è inerente al concetto, ma solo difetti di un'implementazione popolare di quel concetto.
CodesInChaos,

3
Il tuo titolo non corrisponde al corpo della tua domanda. È facile spiegare perché lo stretto accoppiamento di funzioni e dati è negativo, ma il tuo testo pone le domande "OOP fa questo?", "Se sì, perché?" e "È una brutta cosa?". Finora, hai avuto la fortuna di ricevere risposte che trattano di una o più di queste tre domande e nessuna ha assunto la domanda più semplice nel titolo.
itsbruce,

Risposte:


34

In teoria, l'accoppiamento funzione-dati allentato semplifica l'aggiunta di più funzioni per lavorare sugli stessi dati. Il rovescio della medaglia è che rende più difficile cambiare la struttura stessa dei dati, motivo per cui in pratica, un codice funzionale ben progettato e un codice OOP ben progettato hanno livelli di accoppiamento molto simili.

Prendi un grafico aciclico diretto (DAG) come una struttura di dati di esempio. Nella programmazione funzionale, hai ancora bisogno di astrazione per evitare di ripetere te stesso, quindi creerai un modulo con funzioni per aggiungere ed eliminare nodi e bordi, trovare nodi raggiungibili da un dato nodo, creare un ordinamento topologico, ecc. Quelle funzioni sono effettivamente strettamente associati ai dati, anche se il compilatore non li applica. È possibile aggiungere un nodo nel modo più duro, ma perché dovresti farlo? La coesione all'interno di un modulo impedisce l'accoppiamento stretto in tutto il sistema.

Viceversa sul lato OOP, tutte le funzioni diverse dalle operazioni DAG di base verranno eseguite in classi "view" separate, con l'oggetto DAG passato come parametro. È altrettanto facile aggiungere tutte le viste che si desidera che operano sui dati DAG, creando lo stesso livello di disaccoppiamento dati-funzione che si potrebbe trovare nel programma funzionale. Il compilatore non ti impedirà di stipare tutto in una classe, ma lo faranno i tuoi colleghi.

Cambiare i paradigmi di programmazione non cambia le migliori pratiche di astrazione, coesione e accoppiamento, cambia solo le pratiche che il compilatore ti aiuta a far rispettare. Nella programmazione funzionale, quando si desidera l'accoppiamento funzione-dati, viene imposto dall'accordo di gentlemen piuttosto che dal compilatore. In OOP, la separazione della vista modello è imposta dall'accordo dei signori piuttosto che dal compilatore.


13

Nel caso non lo sapessi già, prendi questa intuizione: i concetti di oggetti orientati e chiusure sono due facce della stessa medaglia. Detto questo, cos'è una chiusura? Prende variabili o dati dall'ambito circostante e si lega ad esso all'interno della funzione, oppure da una prospettiva OO si fa effettivamente la stessa cosa quando, ad esempio, si passa qualcosa in un costruttore in modo che in seguito sia possibile utilizzarlo pezzo di dati in una funzione membro di tale istanza. Ma prendere le cose dall'ambito circostante non è una cosa carina da fare: più ampio è l'ambito circostante, più il male è farlo (anche se pragmaticamente, spesso è necessario un po 'di male per portare a termine il lavoro). L'uso di variabili globali lo sta portando all'estremo, in cui le funzioni di un programma utilizzano variabili nell'ambito del programma - davvero molto male. Ci sonobuone descrizioni altrove sul perché le variabili globali sono cattive.

Se segui le tecniche OO, fondamentalmente accetti già che ogni modulo nel tuo programma avrà un certo livello minimo di malvagità. Se adotti un approccio funzionale alla programmazione, stai mirando a un ideale in cui nessun modulo nel tuo programma conterrà chiusura malvagia, anche se potresti averne ancora alcuni, ma sarà molto meno di OO.

Questo è il rovescio della medaglia di OO: incoraggia questo tipo di malvagità, l'accoppiamento dei dati per funzionare attraverso la realizzazione di chiusure standard (una sorta di teoria della programmazione della finestra rotta ).

L'unico lato positivo è che, se sapessi che avresti usato molte chiusure per cominciare, OO ti fornisce almeno un framework idealogico per aiutare a organizzare tale approccio in modo che il programmatore medio possa capirlo. In particolare, le variabili che vengono chiuse sono esplicite nel costruttore anziché semplicemente implicitamente prese in una chiusura di funzione. I programmi funzionali che utilizzano molte chiusure sono spesso più criptici rispetto al programma OO equivalente, anche se non necessariamente meno eleganti :)


8
Citazione del giorno: "un po 'di male è spesso necessario per portare a termine il lavoro"
GlenPeterson,

5
Non hai davvero spiegato perché le cose che chiami male sono cattive; li stai solo chiamando male. Spiega perché sono cattivi e potresti avere una risposta alla domanda del gentiluomo.
Robert Harvey,

2
Il tuo ultimo paragrafo salva però la risposta. Potrebbe essere l'unico lato positivo, secondo te, ma non è poco. Noi cosiddetti "programmatori medi" accogliamo effettivamente una certa cerimonia, sicuramente abbastanza per farci sapere che diavolo sta succedendo.
Robert Harvey,

Se OO e chiusure sono sinonimi, perché così tante lingue OO non sono riuscite a fornire supporto esplicito per loro? La pagina wiki C2 che citi ha ancora più controversie (e meno consenso) di quanto sia normale per quel sito.
itsbruce,

1
@itsbruce Sono resi in gran parte superflui. Le variabili che verrebbero "chiuse" diventano invece variabili di classe passate nell'oggetto.
Izkata,

7

Riguarda l' accoppiamento di tipo :

Una funzione incorporata in un oggetto per lavorare su quell'oggetto non può essere utilizzata su altri tipi di oggetti.

In Haskell si scrivono le funzioni per lavorare contro le classi di tipi - quindi ci sono molti tipi diversi di oggetti su cui una data funzione può lavorare, a patto che sia un tipo della classe data su cui funziona la funzione.

Le funzioni indipendenti consentono tale disaccoppiamento che non si ottiene quando ci si concentra sulla scrittura delle proprie funzioni all'interno del tipo A perché non è possibile utilizzarle se non si dispone di un'istanza di tipo A, anche se la funzione potrebbe altrimenti essere abbastanza generale da essere usato su un'istanza di tipo B o istanza di tipo C.


3
Non è questo il punto delle interfacce? Per fornire le cose che consentono al tipo B e al tipo C di apparire uguali alla tua funzione, in modo che possa operare su più di un tipo?
Casuale 832

2
@ Random832 assolutamente, ma perché incorporare una funzione all'interno di un tipo di dati se non lavorare con quel tipo di dati? La risposta: è l'unica ragione per incorporare una funzione in un tipo di dati. Non potresti scrivere nient'altro che classi statiche e fare in modo che tutte le tue funzioni non si preoccupino del tipo di dati in cui sono incapsulate per renderle completamente disaccoppiate dal loro tipo proprietario, ma allora perché preoccuparsi di metterle in un tipo? L'approccio funzionale dice: non preoccuparti, scrivi le tue funzioni per lavorare verso interfacce di sorta, e quindi non c'è motivo di incapsularle con i tuoi dati.
Jimmy Hoffa,

Devi ancora implementare le interfacce.
Casuale 832,

2
@ Random832 le interfacce sono tipi di dati; non hanno bisogno di funzioni incapsulate in esse. Con le funzioni gratuite, tutte le interfacce che devono essere esaltate sono i dati che rendono disponibili affinché le funzioni lavorino.
Jimmy Hoffa,

2
@ Random832 per relazionarti con oggetti del mondo reale come è così comune in OO, pensa all'interfaccia di un libro: presenta informazioni (dati), tutto qui. Hai la funzione gratuita di turn-page che funziona contro la classe di tipi che hanno pagine, questa funzione funziona contro tutti i tipi di libri, giornali, quei mandrini poster a K-Mart, biglietti di auguri, posta, qualsiasi cosa cucita insieme nel angolo. Se hai implementato turn-page come membro del libro, perdi tutte le cose su cui potresti usare turn-page, in quanto una funzione gratuita non è vincolata; genera solo un'eccezione PartyFoul sulla birra.
Jimmy Hoffa,

4

In Java e simili incarnazioni di OOP, i metodi di istanza (diversamente dalle funzioni libere o dai metodi di estensione) non possono essere aggiunti da altri moduli.

Questo diventa più una limitazione se si considerano le interfacce che possono essere implementate solo con i metodi di istanza. Non è possibile definire un'interfaccia e una classe in moduli diversi e quindi utilizzare il codice di un terzo modulo per collegarli insieme. Un approccio più flessibile, come le classi di tipi di Haskell, dovrebbe essere in grado di farlo.


Puoi farlo facilmente in Scala. Non ho familiarità con Go, ma AFAIK puoi farlo anche lì. In Ruby, è abbastanza comune anche aggiungere metodi agli oggetti dopo il fatto per renderli conformi a qualche interfaccia. Quello che descrivi sembra piuttosto un sistema di tipi mal progettato che qualsiasi cosa anche lontanamente correlata a OO. Proprio come un esperimento mentale: come sarebbe diversa la tua risposta quando parli di tipi di dati astratti anziché di oggetti? Non credo che farebbe alcuna differenza, il che proverebbe che il tuo argomento non è correlato a OO.
Jörg W Mittag,

1
@ JörgWMittag Penso che intendessi tipi di dati algebrici. E CodesInChaos, Haskell scoraggia in modo molto esplicito ciò che stai suggerendo. Si chiama un'istanza orfana e invia avvisi su GHC.
Daniel Gratzer,

3
@ JörgWMittag La mia impressione è che molti che criticano OOP criticano la forma di OOP usata in Java e in linguaggi simili con la sua rigida struttura di classe e si concentrano sui metodi di istanza. La mia impressione di quella citazione è che critica l'attenzione sui metodi di istanza e non si applica realmente ad altri sapori di OOP, come quello che usa il Golang.
CodesInCos

2
@CodesInChaos Quindi forse chiarire questo come "OO basato sulla classe statica"
Daniel Gratzer

@jozefg: sto parlando di tipi di dati astratti. Non vedo nemmeno come i tipi di dati algebrici siano remotamente rilevanti per questa discussione.
Jörg W Mittag,

3

L'orientamento agli oggetti riguarda fondamentalmente l'astrazione di dati procedurali (o astrazione di dati funzionali se si eliminano gli effetti collaterali che sono un problema ortogonale). In un certo senso, Lambda Calculus è il linguaggio orientato agli oggetti più antico e puro, poiché fornisce solo astrazione di dati funzionali (perché non ha costrutti oltre alle funzioni).

Solo le operazioni di un singolo oggetto possono ispezionare la rappresentazione dei dati di quell'oggetto. Neanche altri oggetti dello stesso tipo possono farlo. (Questa è la differenza principale tra l'astrazione dei dati orientata agli oggetti e i tipi di dati astratti: con gli ADT, gli oggetti dello stesso tipo possono ispezionare la rappresentazione dei dati degli altri, solo la rappresentazione di oggetti di altri tipi è nascosta.)

Ciò significa che diversi oggetti dello stesso tipo possono avere rappresentazioni di dati differenti. Anche lo stesso oggetto può avere rappresentazioni di dati diversi in momenti diversi. (Ad esempio, in Scala, MapS e Sets passare da una matrice e un trie hash a seconda del numero di elementi perché per numeri molto piccoli lineare ricerca in una matrice è più veloce della ricerca logaritmica in un albero di ricerca a causa delle piccole costanti .)

Dall'esterno di un oggetto, non dovresti, non puoi conoscere la sua rappresentazione dei dati. Questo è l' opposto dell'accoppiamento stretto.


Ho delle classi in OOP che cambiano le strutture di dati interne a seconda delle circostanze, quindi le istanze di oggetti di queste classi possono usare contemporaneamente rappresentazioni dei dati molto diverse. Nascondere e incapsulare i dati di base direi? Quindi, in che modo Map in Scala è diverso da una classe Map implementata correttamente (wrt data hiding and encapsulation) in un linguaggio OOP?
Marjan Venema,

Nel tuo esempio, incapsulare i tuoi dati con funzioni accessor in una classe (e quindi accoppiare strettamente quelle funzioni a quei dati) in realtà ti consente di accoppiare liberamente istanze di quella classe con il resto del tuo programma. Stai confutando il punto centrale della citazione - molto bello!
GlenPeterson,

2

Lo stretto accoppiamento tra dati e funzioni è negativo perché si desidera poter cambiare l'uno indipendentemente dall'altro e l'accoppiamento stretto lo rende difficile perché non è possibile cambiarne uno senza la conoscenza dell'altro e possibilmente modificarlo.

Volete dati diversi presentati alla funzione per non richiedere alcuna modifica nella funzione e allo stesso modo volete essere in grado di apportare modifiche alla funzione senza bisogno di alcuna modifica ai dati su cui sta operando per supportare quei cambiamenti di funzione.


1
Sì, lo voglio. Ma la mia esperienza è che quando si inviano dati a una funzione non banale che non è stata esplicitamente progettata per gestire, quella funzione tende a non funzionare. Non mi riferisco solo alla sicurezza dei tipi, ma piuttosto a qualsiasi condizione di dati che non è stata anticipata dagli autori della funzione. Se la funzione è obsoleta e spesso utilizzata, qualsiasi modifica che consenta il flusso di nuovi dati è probabile che la interrompa per qualche vecchia forma di dati che deve ancora funzionare. Mentre il disaccoppiamento può essere l'ideale per funzioni rispetto ai dati, la realtà di tale disaccoppiamento può essere difficile e pericolosa.
GlenPeterson,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.