Google ha sollevato una domanda simile con una risposta che ritengo molto valida. L'ho citato di seguito.
C'è un'altra distinzione in agguato qui che è spiegata nel saggio Cook che ho collegato.
Gli oggetti non sono l'unico modo per implementare l'astrazione. Non tutto è un oggetto. Gli oggetti implementano qualcosa che alcune persone chiamano astrazione di dati procedurali. I tipi di dati astratti implementano una diversa forma di astrazione.
Una differenza chiave appare quando si considerano metodi / funzioni binari. Con l'astrazione di dati procedurali (oggetti), potresti scrivere qualcosa del genere per un'interfaccia Int set:
interface IntSet {
void unionWith(IntSet s);
...
}
Consideriamo ora due implementazioni di IntSet, ad esempio una supportata da elenchi e una supportata da una struttura ad albero binario più efficiente:
class ListIntSet implements IntSet {
void unionWith(IntSet s){ ... }
}
class BSTIntSet implements IntSet {
void unionWith(IntSet s){ ... }
}
Si noti che unionWith deve accettare un argomento IntSet. Non il tipo più specifico come ListIntSet o BSTIntSet. Ciò significa che l'implementazione di BSTIntSet non può presumere che il suo input sia un BSTIntSet e utilizzare tale fatto per fornire un'implementazione efficiente. (Potrebbe utilizzare alcune informazioni sul tipo di runtime per verificarlo e, se lo è, utilizzare un algoritmo più efficiente, ma potrebbe comunque essere passato a un ListIntSet e dovrà ricorrere a un algoritmo meno efficiente).
Confronta questo con gli ADT, dove puoi scrivere qualcosa di più simile al seguente in un file di firma o di intestazione:
typedef struct IntSetStruct *IntSetType;
void union(IntSetType s1, IntSetType s2);
Programmiamo contro questa interfaccia. In particolare, il tipo viene lasciato astratto. Non puoi sapere di cosa si tratta. Quindi abbiamo un'implementazione BST quindi fornisce un tipo e operazioni concrete:
struct IntSetStruct {
int value;
struct IntSetStruct* left;
struct IntSetStruct* right;
}
void union(IntSetType s1, IntSetType s2){ ... }
Ora Union in realtà conosce le rappresentazioni concrete sia di s1 che di s2, quindi può sfruttarlo per un'implementazione efficiente. Possiamo anche scrivere una implementazione supportata da un elenco e scegliere invece di collegarci con quella.
Ho scritto la sintassi C (ish), ma dovresti guardare ad esempio ML standard per i tipi di dati astratti fatti correttamente (dove puoi ad esempio usare più di un'implementazione di un ADT nello stesso programma approssimativamente qualificando i tipi: BSTImpl. IntSetStruct e ListImpl.IntSetStruct, diciamo)
Il contrario è che l'astrazione (oggetti) procedurale dei dati consente di introdurre facilmente nuove implementazioni che funzionano con quelle precedenti. ad esempio puoi scrivere la tua implementazione LoggingIntSet personalizzata e unirla a un BSTIntSet. Ma questo è un compromesso: perdi tipi informativi per i metodi binari! Spesso si finisce per esporre più funzionalità e dettagli di implementazione nella propria interfaccia di quanto si farebbe con un'implementazione ADT. Ora mi sento come se stessi solo scrivendo nuovamente il saggio di Cook, quindi davvero, leggilo!
Vorrei aggiungere un esempio a questo.
Cook suggerisce che un esempio di un tipo di dati astratto è un modulo in C. In effetti, i moduli in C implicano il nascondere informazioni, poiché ci sono funzioni pubbliche che vengono esportate attraverso un file di intestazione e funzioni statiche (private) che non lo fanno. Inoltre, spesso ci sono costruttori (ad esempio list_new ()) e osservatori (ad esempio list_getListHead ()).
Un punto chiave di ciò che rende, diciamo, un modulo elenco chiamato LIST_MODULE_SINGLY_LINKED un ADT è che le funzioni del modulo (ad esempio list_getListHead ()) presuppongono che i dati immessi siano stati creati dal costruttore di LIST_MODULE_SINGLY_LINKED, al contrario di qualsiasi "equivalente "implementazione di un elenco (ad es. LIST_MODULE_DYNAMIC_ARRAY). Ciò significa che le funzioni di LIST_MODULE_SINGLY_LINKED possono assumere, nella loro implementazione, una rappresentazione particolare (ad esempio un elenco collegato singolarmente).
LIST_MODULE_SINGLY_LINKED non può interagire con LIST_MODULE_DYNAMIC_ARRAY perché non possiamo alimentare i dati creati, diciamo con il costruttore di LIST_MODULE_DYNAMIC_ARRAY, all'osservatore di LIST_MODULE_SINGLY_LINKED perché un LIST_MODE presuppone un oggetto
Questo è analogo a un modo in cui due diversi gruppi dall'algebra astratta non possono interagire (cioè, non puoi prendere il prodotto di un elemento di un gruppo con un elemento di un altro gruppo). Questo perché i gruppi assumono la proprietà di chiusura del gruppo (il prodotto degli elementi in un gruppo deve essere nel gruppo). Tuttavia, se possiamo dimostrare che due diversi gruppi sono in realtà sottogruppi di un altro gruppo G, allora possiamo usare il prodotto di G per aggiungere due elementi, uno per ciascuno dei due gruppi.
Confronto tra ADT e oggetti
Cook lega la differenza tra ADT e oggetti parzialmente al problema dell'espressione. In parole povere, gli ADT sono associati a funzioni generiche che sono spesso implementate in linguaggi di programmazione funzionale, mentre gli oggetti sono accoppiati a "oggetti" Java a cui si accede tramite interfacce. Ai fini di questo testo, una funzione generica è una funzione che accetta alcuni argomenti ARGS e un tipo TYPE (pre-condizione); in base a TIPO seleziona la funzione appropriata e la valuta con ARGS (post-condizione). Sia le funzioni generiche che gli oggetti implementano il polimorfismo, ma con funzioni generiche, il programmatore CONOSCE quale funzione sarà eseguita dalla funzione generica senza guardare il codice della funzione generica. Con gli oggetti d'altra parte, il programmatore non sa come l'oggetto gestirà gli argomenti, a meno che i programmatori non guardino il codice dell'oggetto.
Di solito si pensa al problema dell'espressione in termini di "ho molte rappresentazioni?" vs. "ho molte funzioni con poca rappresentazione". Nel primo caso si dovrebbe organizzare il codice in base alla rappresentazione (come è più comune, specialmente in Java). Nel secondo caso si dovrebbe organizzare il codice in base alle funzioni (ovvero avere un'unica funzione generica gestire più rappresentazioni).
Se organizzi il tuo codice in base alla rappresentazione, quindi, se vuoi aggiungere funzionalità extra, sei costretto ad aggiungere la funzionalità a ogni rappresentazione dell'oggetto; in questo senso aggiungere funzionalità non è "additivo". Se organizzi il tuo codice in base alla funzionalità, quindi, se desideri aggiungere una rappresentazione aggiuntiva, sei costretto ad aggiungere la rappresentazione a ogni oggetto; in questo senso aggiungendo rappresentazioni non "additive".
Vantaggio degli ADT sugli oggetti
L'aggiunta di funzionalità è additiva
Possibile sfruttare la conoscenza della rappresentazione di un ADT per le prestazioni, o per dimostrare che l'ADT garantirà un certo postcondizionamento dato un prerequisito. Ciò significa che la programmazione con gli ADT consiste nel fare le cose giuste nel giusto ordine (concatenando pre-condizioni e post-condizioni verso una condizione post "obiettivo").
Vantaggi degli oggetti rispetto agli ADT
Aggiunta di rappresentazioni in additivo
Gli oggetti possono interagire
È possibile specificare le condizioni pre / post per un oggetto e unirle insieme come nel caso degli ADT. In questo caso, i vantaggi degli oggetti sono che (1) è facile cambiare le rappresentazioni senza cambiare l'interfaccia e (2) gli oggetti possono interagire. Tuttavia, ciò sconfigge lo scopo di OOP nel senso di smalltalk. (vedere la sezione "La versione di OOP di Alan Kay)
L'invio dinamico è la chiave per OOP
Dovrebbe essere evidente ora che l'invio dinamico (ovvero l'associazione tardiva) è essenziale per la programmazione orientata agli oggetti. Questo è così che è possibile definire le procedure in modo generico, che non assume una rappresentazione particolare. Per essere concreti - la programmazione orientata agli oggetti è facile in Python, perché è possibile programmare i metodi di un oggetto in un modo che non assume una rappresentazione particolare. Ecco perché python non ha bisogno di interfacce come Java.
In Java, le classi sono ADT. tuttavia, una classe a cui si accede tramite l'interfaccia implementata è un oggetto.
Addendum: la versione di OOP di Alan Kay
Alan Kay si riferiva esplicitamente agli oggetti come "famiglie di algebre", e Cook suggerisce che un ADT è un'algebra. Quindi Kay probabilmente intendeva dire che un oggetto è una famiglia di ADT. Cioè, un oggetto è la raccolta di tutte le classi che soddisfano un'interfaccia Java.
Tuttavia, l'immagine degli oggetti dipinti da Cook è molto più restrittiva della visione di Alan Kay. Voleva che gli oggetti si comportassero come computer in una rete o come cellule biologiche. L'idea era di applicare il principio del minimo impegno nella programmazione, in modo che sia facile cambiare i livelli di basso livello di un ADT una volta che i livelli di alto livello sono stati costruiti usando loro. Tenendo presente questa immagine, le interfacce Java sono troppo restrittive perché non consentono a un oggetto di interpretare il significato di un messaggio o addirittura di ignorarlo completamente.
In sintesi, l'idea chiave degli oggetti, per Kay - non è che siano una famiglia di algebre (come sottolineato da Cook). Piuttosto, l'idea chiave di Kay era applicare un modello che funzionasse nel grande (computer in una rete) al piccolo (oggetti in un programma).
modifica: un altro chiarimento sulla versione di Kay di OOP: lo scopo degli oggetti è avvicinarsi a un ideale dichiarativo. Dovremmo dire all'oggetto cosa fare, non dirlo come tramite lo stato di micromanaging, come è consuetudine con la programmazione procedurale e gli ADT. Maggiori informazioni possono essere trovate qui , qui , qui e qui .
edit: ho trovato molto, molto buona esposizione della definizione di Alan Kay di OOP qui .