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

38

Ho trovato questa citazione in " La gioia di 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] downside to object-oriented programming is the tight coupling between function and data.

Capisco perché l'accoppiamento non necessario è cattivo 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 sulle classi sia intrinsecamente sbagliato.

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

Se questo è un attacco velato sui tipi, perché non dicono semplicemente che limitare il tipo di input e output a una funzione è sbagliato? Non sono sicuro di poter essere d'accordo, ma almeno ho familiarità con argomenti pro e con type safety. 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 inconveniente molto piccolo.

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

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

    
posta GlenPeterson 25.09.2013 - 14:52
fonte

6 risposte

33

In teoria, l'accoppiamento dati-funzione è più semplice per aggiungere più funzioni per lavorare sugli stessi dati. Il lato negativo rende più difficile modificare la struttura dati stessa, motivo per cui in pratica un codice funzionale ben progettato e un codice OOP ben progettato presentano livelli di accoppiamento molto simili.

Prendi un grafico aciclico diretto (DAG) come struttura 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 spigoli, trovare nodi raggiungibili da un determinato nodo, creare un ordinamento topologico, ecc. Tali funzioni sono strettamente accoppiati ai dati, anche se il compilatore non lo applica. Puoi aggiungere un nodo nel modo più difficile, ma perché vorresti 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 separate "view", con l'oggetto DAG passato come parametro. È altrettanto facile aggiungere quante visualizzazioni vuoi che operino sui dati DAG, creando lo stesso livello di disaccoppiamento di funzione-dati che si troverà nel programma funzionale. Il compilatore non ti impedirà di riempire tutto in un'unica classe, ma i tuoi colleghi lo faranno.

La modifica dei paradigmi di programmazione non cambia le migliori pratiche di astrazione, coesione e accoppiamento, ma modifica solo le pratiche che il compilatore ti aiuta ad applicare. Nella programmazione funzionale, quando si desidera l'accoppiamento dati-funzione, viene applicato dal gentlemen's agreement piuttosto che dal compilatore. In OOP, la separazione della vista del modello viene applicata dal gentlemen's agreement piuttosto che dal compilatore.

    
risposta data 25.09.2013 - 16:49
fonte
13

Nel caso in cui non lo sapessi, prendi già questa idea: i concetti di object-oriented e closures sono due lati della stessa medaglia Detto questo, cos'è una chiusura? Prende variabili (s) o dati dall'ambito circostante e si lega ad esso all'interno della funzione, o da una prospettiva OO si fa effettivamente la stessa cosa quando, ad esempio, si passa qualcosa in un costruttore in modo che in seguito si possa usare quello pezzo di dati in una funzione membro di quell'istanza. Ma prendere le cose dall'ambito circostante non è una cosa carina da fare - quanto più ampio è il raggio d'azione circostante, tanto più è malvagio farlo (anche se pragmaticamente, alcuni aspetti del male sono spesso necessari per ottenere il lavoro svolto). L'uso di variabili globali sta portando questo all'estremo, dove le funzioni in un programma stanno usando variabili allo scopo del programma - davvero davvero malvagie. Ci sono buone descrizioni altrove sul perché le variabili globali sono malvagie.

Se segui le tecniche OO, sostanzialmente accetti che ogni modulo del tuo programma avrà un certo livello minimo di malvagità. Se segui un approccio funzionale alla programmazione, stai mirando a un ideale in cui nessun modulo del tuo programma conterrà la malvagità della chiusura, anche se potresti ancora averne 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 rendendo le chiusure standard (una specie di teoria delle finestre rotte di programmazione).

L'unico lato positivo è che, se sapessi che avresti dovuto usare molte chiusure per iniziare, OO ti fornisce almeno un quadro ideale per aiutare a organizzare tale approccio in modo che il programmatore medio possa capirlo. In particolare, le variabili che vengono chiuse sopra sono esplicite nel costruttore piuttosto che solo implicitamente implicite in una chiusura di funzione. I programmi funzionali che utilizzano molte chiusure sono spesso più criptici del programma OO equivalente, anche se non necessariamente meno eleganti:)

    
risposta data 25.09.2013 - 16:56
fonte
7

Riguarda l'accoppiamento 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 tipo classi - quindi ci sono molti diversi tipi di oggetti a cui una determinata funzione può lavorare, purché sia un tipo della classe quella funzione funziona.

Le funzioni indipendenti consentono tale disaccoppiamento che non ottieni quando ti concentri sulla scrittura delle tue funzioni per lavorare all'interno del tipo A, perché non puoi usarle se non hai un'istanza di tipo A, anche se altrimenti la funzione potrebbe essere abbastanza generale da essere utilizzata su un'istanza di tipo B o su un'istanza di tipo C.

    
risposta data 25.09.2013 - 17:48
fonte
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ù di una restrizione quando si considerano interfacce che possono essere implementate solo dai metodi di istanza. Non è possibile definire un'interfaccia e una classe in moduli diversi e quindi utilizzare il codice di un terzo modulo per unirli tra loro. Un approccio più flessibile, come le classi di tipi di Haskell dovrebbero essere in grado di farlo.

    
risposta data 25.09.2013 - 15:26
fonte
3

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

Solo le operazioni di un oggetto singolo possono ispezionare la rappresentazione dei dati dell'oggetto. Nemmeno altri oggetti dello stesso tipo possono farlo. (Questa è la principale differenza 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 degli 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 differenti in momenti diversi. (Ad esempio, in Scala, Map s e Set s commutano tra una matrice e un trie hash a seconda del numero di elementi perché per numeri molto piccoli la ricerca lineare in una matrice è più veloce della ricerca logaritmica in una struttura di ricerca perché dei fattori costanti molto piccoli.)

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

    
risposta data 25.09.2013 - 16:07
fonte
1

Lo stretto accoppiamento tra dati e funzioni è sbagliato perché vuoi essere in grado di cambiarne ciascuno indipendentemente dall'altra e l'accoppiamento stretto lo rende difficile perché non puoi cambiarne uno senza la conoscenza e l'eventuale cambiamento, l'altro.

Si desidera che dati diversi presentati alla funzione non richiedano modifiche nella funzione e allo stesso modo si desideri essere in grado di apportare modifiche alla funzione senza bisogno di modifiche ai dati su cui sta operando per supportare tali cambiamenti di funzione.

    
risposta data 25.09.2013 - 17:31
fonte

Leggi altre domande sui tag