Quali problemi di programmazione procedurale risolve in pratica OOP?


17

Ho studiato il libro "C ++ Demystified" . Ora ho iniziato a leggere "Programmazione orientata agli oggetti nella prima edizione Turbo C ++ (1a edizione)" di Robert Lafore. Non ho alcuna conoscenza della programmazione che vada oltre questi libri. Questo libro potrebbe essere obsoleto perché ha 20 anni. Ho l'ultima edizione, sto usando la vecchia perché mi piace, principalmente sto solo studiando i concetti di base di OOP usati in C ++ attraverso la prima edizione del libro di Lafore.

Il libro di Lafore sottolinea che "OOP" è utile solo per programmi più grandi e complessi . Si dice in ogni libro di OOP (anche nel libro di Lafore) che il paradigma procedurale è soggetto a errori, ad esempio i dati globali, facilmente vulnerabili dalle funzioni. Si dice che il programmatore possa commettere errori onesti nei linguaggi procedurali, ad esempio facendo una funzione che corrompe accidentalmente i dati.

Onestamente, sto pubblicando la mia domanda perché non sto afferrando la spiegazione fornita in questo libro: Programmazione orientata agli oggetti in C ++ (4a edizione) Non afferro queste affermazioni scritte nel libro di Lafore:

La programmazione orientata agli oggetti è stata sviluppata perché sono stati scoperti limiti nei precedenti approcci alla programmazione .... Man mano che i programmi diventano sempre più grandi e complessi, anche l'approccio strutturato alla programmazione inizia a mostrare segni di tensione ... .... Analizzare le ragioni di questi fallimenti rivelano che ci sono punti deboli nel paradigma procedurale stesso. Non importa quanto bene sia implementato l'approccio di programmazione strutturata, i grandi programmi diventano eccessivamente complessi .... ... Ci sono due problemi correlati. Innanzitutto, le funzioni hanno accesso illimitato ai dati globali. Secondo, funzioni e dati non correlati, la base del paradigma procedurale, forniscono un modello povero del mondo reale ...

Ho studiato il libro "dismistificato C ++" di Jeff Kent, mi piace molto questo libro, in questo libro viene principalmente spiegata la programmazione procedurale. Non capisco perché la programmazione procedurale (strutturata) sia debole!

Il libro di Lafore spiega molto bene il concetto con alcuni buoni esempi. Inoltre ho colto un'intuizione leggendo il libro di Lafore secondo cui OOP è migliore della programmazione procedurale, ma sono curioso di sapere come, nella pratica, la programmazione procedurale sia più debole di OOP.

Voglio vedermi quali sono i problemi pratici che si potrebbero incontrare nella programmazione procedurale, in che modo l'OOP renderà la programmazione più semplice. Penso che otterrò la mia risposta solo leggendo il libro di Lafore in modo contemplativo, ma voglio vedere con i miei occhi i problemi nel codice procedurale, voglio vedere come il codice di stile OOP di un programma rimuove gli errori che si verificano se il lo stesso programma doveva essere scritto usando il paradigma procedurale.

Ci sono molte funzionalità di OOP e capisco che non è possibile che qualcuno mi spieghi come tutte queste funzionalità rimuovono gli errori di cui sopra che genererebbero scrivendo il codice in stile procedurale.

Quindi questa è la mia domanda:

Quali sono le limitazioni della programmazione procedurale affrontate da OOP e in che modo le elimina nella pratica?

In particolare, ci sono esempi per programmi che sono difficili da progettare usando il paradigma procedurale ma che sono facilmente progettati usando OOP?

PS: Cross pubblicato da: /programming//q/22510004/3429430


3
La distinzione tra programmazione procedurale e programmazione orientata agli oggetti è in una certa misura una questione di presentazione ed enfasi. La maggior parte delle lingue che si pubblicizzano come orientate agli oggetti sono anche procedurali: i termini guardano a diversi aspetti della lingua.
Gilles 'SO- smetti di essere malvagio' il

2
Ho riaperto la domanda ora; vediamo come va. Tieni presente che qualsiasi trattamento che presenta OOP è un Santo Graal, risolve tutti i problemi del messia dei linguaggi di programmazione che sta dando senso. Ci sono pro e contro. Stai chiedendo i professionisti, ed è una domanda giusta; non aspettarti di più.
Raffaello

Il pensiero di progettare una GUI desktop (moderna) senza OOP e alcune tecniche che sono cresciute su di essa (ad esempio pattern di eventi / ascoltatori) mi spaventa. Ciò non significa che non possa essere fatto (certamente può ), però. Inoltre, se vuoi vedere il fallimento di PP al lavoro, guarda PHP e confronta l'API con, diciamo, Ruby.
Raffaello

1
"Non importa quanto bene l'approccio programmazione strutturata viene implementata, programmi di grandi dimensioni diventano eccessivamente complessa ..." che è molto vero di programmazione orientata agli oggetti anche, ma fondamentalmente fa gestire la complessità meglio se applicato nel modo prescritto dagli sviluppatori ... e lo fa in gran parte attraverso limiti / restrizioni dell'ambito migliori / più nitidi, cioè una sorta di sistema di compartimentazione ... aka APIE: astrazione, polimorfismo, ereditarietà, incapsulamento
vzn

Risposte:


9

In un linguaggio procedurale non è necessariamente possibile esprimere le restrizioni necessarie per dimostrare che il chiamante utilizza un modulo in modo supportato. In assenza di una limitazione verificabile del compilatore, è necessario scrivere la documentazione e sperare che venga seguita e utilizzare i test unitari per dimostrare gli usi previsti.

Dichiarare i tipi è la restrizione dichiarativa più ovvia (cioè: "dimostrare che x è un float"). Costringere le mutazioni di dati a passare attraverso una funzione nota per essere progettata per quei dati è un'altra. L'applicazione del protocollo (metodo invocare l'ordinamento) è un'altra restrizione parzialmente supportata, ad esempio: "costruttore -> altri metodi * -> distruttore".

Ci sono anche vantaggi reali (e alcuni svantaggi) quando il compilatore conosce il modello. La tipizzazione statica con tipi polimorfici è un po 'un problema quando si emula l'incapsulamento dei dati da un linguaggio procedurale. Per esempio:

il tipo x1 è un sottotipo di x, t1 è un sottotipo di t

Questo è un modo per incapsulare i dati in un linguaggio procedurale per avere un tipo t con i metodi feg, e una sottoclasse t1 che fa allo stesso modo:

t_f (t, x, y, z, ...), t_g (t, x, y, ...) t1_f (t1, x, y, z, ...)

Per utilizzare questo codice così com'è, devi fare un controllo del tipo e attivare il tipo di t prima di decidere il tipo di f che invocherai. Potresti aggirarlo in questo modo:

digitare t {d: data f: funzione g: funzione}

In modo da invocare invece tf (x, y, z), dove un typecheck e il passaggio per trovare il metodo sono ora sostituiti con il fatto che ogni istanza memorizzi in modo esplicito i puntatori del metodo. Ora, se hai un numero enorme di funzioni per tipo, questa è una rappresentazione dispendiosa. Potresti quindi utilizzare un'altra strategia come t che punta a una variabile m che contiene tutte le funzioni membro. Se questa funzionalità fa parte del linguaggio, è possibile consentire al compilatore di capire come gestire una rappresentazione efficiente di questo modello.

Ma l'incapsulamento dei dati è il riconoscimento che lo stato mutabile è negativo. La soluzione orientata agli oggetti è nasconderla dietro i metodi. Idealmente, tutti i metodi in un oggetto avrebbero un ordine di chiamata ben definito (es .: costruttore -> apri -> [leggi | scrivi] -> chiudi -> distruggi); che a volte viene chiamato un "protocollo" (ricerca: "Microsoft Singularity"). Ma al di là della costruzione e della distruzione, questi requisiti non fanno generalmente parte del sistema dei tipi, né sono ben documentati. In questo senso, gli oggetti sono istanze simultanee di macchine a stati che sono trasferite da chiamate di metodo; tale che potresti avere più istanze e usarle in modo arbitrariamente intercalato.

Ma riconoscendo che lo stato condiviso mutabile è negativo, si può notare che l'orientamento agli oggetti può creare un problema di concorrenza poiché la struttura dei dati degli oggetti è uno stato mutabile a cui molti oggetti hanno un riferimento. La maggior parte dei linguaggi orientati agli oggetti viene eseguita sul thread del chiamante, il che significa che ci sono condizioni di competizione nelle invocazioni del metodo; per non parlare delle sequenze non atomiche di chiamate di funzioni. In alternativa, ogni oggetto potrebbe ottenere messaggi asincroni in una coda e servirli tutti sul thread dell'oggetto (con i suoi metodi privati) e rispondere al chiamante inviandogli messaggi.

Confronta le chiamate al metodo Java in un contesto multi-thread con i processi Erlang che si inviano messaggi (che fanno riferimento solo a valori immutabili).

L'orientamento agli oggetti senza restrizioni in combinazione con il parallelismo è un problema dovuto al blocco. Esistono tecniche che vanno dalla memoria transazionale software (es .: transazioni ACID su oggetti di memoria simili ai database) all'utilizzo di un approccio di "condivisione di memoria mediante comunicazione (dati immutabili)" (ibrido di programmazione funzionale).

A mio avviso, la letteratura sull'orientamento agli oggetti focalizza FAR troppo sull'eredità e non abbastanza sul protocollo (ordinamento di invocazione del metodo controllabile, condizioni preliminari, postcondizioni, ecc.). L'input che consuma un oggetto dovrebbe di solito avere una grammatica ben definita, esprimibile come tipo.


Stai dicendo che nei linguaggi OO, il compilatore può verificare se i metodi sono utilizzati nell'ordine prescritto o altre restrizioni sull'uso dei moduli? Perché "l'incapsulamento dei dati [...] riconosce che lo stato mutabile è negativo"? Quando parli di polimorfismo, stai assumendo che stai usando un linguaggio OO?
babou,

In OO, la capacità più importante è quella di essere in grado di nascondere la struttura dei dati (ovvero: richiedere che i riferimenti siano come this.x) per dimostrare che tutti gli accessi passano attraverso i metodi. In un linguaggio OO tipicamente statico, stai dichiarando ancora più restrizioni (in base ai tipi). La nota sull'ordinamento dei metodi sta solo dicendo che OO forza il costruttore a essere chiamato per primo e il distruttore per ultimo; che è il primo passo per vietare strutturalmente ordini di chiamata errati. Le prove facili sono un obiettivo importante nella progettazione del linguaggio.
Rob

8

La programmazione procedurale / funzionale non è in alcun modo più debole di OOP , anche senza entrare negli argomenti di Turing (il mio linguaggio ha il potere di Turing e può fare qualsiasi cosa faccia un altro), il che non significa molto. In realtà, le tecniche orientate agli oggetti sono state inizialmente sperimentate in linguaggi che non li avevano integrati. In questo senso, la programmazione OO è solo uno stile specifico di programmazione procedurale . Ma aiuta a far rispettare discipline specifiche, come la modularità, l'astrazione e il nascondere le informazioni che sono essenziali per la comprensione e la manutenzione del programma.

Alcuni paradigmi di programmazione si evolvono dalla visione teorica del calcolo. Un linguaggio come Lisp si è evoluto dal lambda-calcolo e dall'idea della meta-circolarità dei linguaggi (simile alla riflessività nel linguaggio naturale). Le clausole di Horn generarono Prolog e la programmazione dei vincoli. Anche la famiglia Algol deve il lambda-calcolo, ma senza riflessività integrata.

Lisp è un esempio interessante, in quanto è stato il banco di prova di molte innovazioni del linguaggio di programmazione, riconducibile al suo doppio patrimonio genetico.

Tuttavia, le lingue si evolvono, spesso con nuovi nomi. Un importante fattore di evoluzione è la pratica di programmazione. Gli utenti identificano le pratiche di programmazione che migliorano le proprietà dei programmi come leggibilità, manutenibilità, verificabilità della correttezza. Quindi cercano di aggiungere alle lingue funzionalità o vincoli che supporteranno e talvolta applicheranno queste pratiche in modo da migliorare la qualità dei programmi.

Ciò significa che queste pratiche sono già possibili nel vecchio linguaggio di programmazione, ma ci vuole comprensione e disciplina per usarle. Incorporarli in nuovi linguaggi come concetti primari con sintassi specifica rende queste pratiche più facili da usare e da comprendere prontamente, in particolare per gli utenti meno sofisticati (cioè la stragrande maggioranza). Inoltre rende la vita un po 'più semplice per gli utenti sofisticati.

In qualche modo, è progettare la lingua che cosa è un sottoprogramma / funzione / procedura per un programma. Una volta identificato il concetto utile, gli viene dato un nome (possibilmente) e una sintassi, in modo che possa essere facilmente utilizzato in tutti i programmi sviluppati con quella lingua. E quando avrà successo, sarà incorporato anche nelle lingue future.

Esempio: ricreare l'orientamento agli oggetti

Ora provo a illustrarlo su un esempio (che potrebbe certamente essere ulteriormente perfezionato, visto il tempo). Lo scopo dell'esempio non è mostrare che un programma orientato agli oggetti può essere scritto in uno stile di programmazione procedurale, possibilmente a scapito della responsabilità e della manutenibilità. Cercherò piuttosto di mostrare che alcune lingue senza strutture OO possono effettivamente utilizzare funzioni di ordine superiore e struttura di dati per creare effettivamente i mezzi per imitare in modo efficace l'orientamento agli oggetti , al fine di beneficiare delle sue qualità in materia di organizzazione del programma, tra cui modularità, astrazione e occultamento delle informazioni .

Come ho detto, Lisp è stato il banco di prova di molte evoluzioni linguistiche, incluso il paradigma OO (sebbene quella che potesse essere considerata la prima lingua OO era Simula 67, nella famiglia Algol). Lisp è molto semplice e il codice per il suo interprete di base è inferiore a una pagina. Ma puoi fare la programmazione OO in Lisp. Tutto ciò che serve è funzioni di ordine superiore.

Non userò la sintassi esoterica di Lisp, ma piuttosto lo pseudo-codice, per semplificare la presentazione. E considererò un semplice problema essenziale: nascondere le informazioni e la modularità . Definire una classe di oggetti impedendo all'utente di accedere (la maggior parte) dell'implementazione.

Supponiamo che io voglia creare una classe chiamata vettore, che rappresenta vettori bidimensionali, con metodi che includono: aggiunta di vettore, dimensione di vettore e parallelismo.

function vectorrec () {  
  function createrec(x,y) { return [x,y] }  
  function xcoordrec(v) { return v[0] }  
  function ycoordrec(v) { return v[1] }  
  function plusrec (u,v) { return [u[0]+v[0], u[1]+v[1]] }  
  function sizerec(v) { return sqrt(v[0]*v[0]+v[1]*v[1]) }  
  function parallelrec(u,v) { return u[0]*v[1]==u[1]*v[0]] }  
  return [createrec, xcoordrec, ycoordrec, plusrec, sizerec, parallelrec]  
  }  

Quindi posso assegnare il vettore creato ai nomi delle funzioni effettive da utilizzare.

[vector, xcoord, ycoord, vplus, vsize, vparallel] = vectorclass ()

Perché essere così complicato? Perché posso definire nella funzione costrutti intermedi vectorrec che non voglio essere visibile al resto del programma, in modo da preservare la modularità.

Possiamo fare un'altra raccolta in coordinate polari

function vectorpol () {  
  ...  
  function pluspol (u,v) { ... }  
  function sizepol (v) { return v[0] }  
  ...  
  return [createpol, xcoordpol, ycoordpol, pluspol, sizepol, parallelpol]  
  }  

Ma potrei voler usare indifferentemente entrambe le implementazioni. Un modo per farlo è aggiungere un componente di tipo a tutti i valori e definire tutte le funzioni di cui sopra nello stesso ambiente: Quindi posso definire ciascuna delle funzioni restituite in modo che testerà prima il tipo di coordinate, quindi applichi la funzione specifica per questo.

function vector () {  
    ...  
    function plusrec (u,v) { ... }  
    ...  
    function pluspol (u,v) { ... }  
    ...  
    function plus (u,v) { if u[2]='rec' and v[2]='rec'  
                            then return plusrec (u,v) ... }  

    return [ ..., plus, ...]  
    }

Cosa ho guadagnato: le funzioni specifiche rimangono invisibili (a causa dell'ambito degli identificatori locali) e il resto del programma può utilizzare solo le più astratte restituite dalla chiamata a vectorclass.

Un'obiezione è che potrei definire direttamente ciascuna delle funzioni astratte nel programma e lasciare all'interno della definizione delle funzioni dipendenti dal tipo di coordinata. Quindi sarebbe anche nascosto. Questo è vero, ma poi il codice per ciascun tipo di coordinate verrebbe tagliato in piccoli pezzi distribuiti sul programma, che è meno ridimensionabile e gestibile.

In realtà, non ho nemmeno bisogno di dare loro un nome e potrei semplicemente mantenere i valori funzionali anonimi in una struttura di dati indicizzata dal tipo e una stringa che rappresenta il nome della funzione. Questa struttura essendo locale al vettore di funzione sarebbe invisibile dal resto del programma.

Per semplificare l'uso, invece di restituire un elenco di funzioni, posso restituire una singola funzione chiamata apply prendendo come argomento un valore di tipo esplicito e una stringa e applicare la funzione con il tipo e il nome corretti. Questo assomiglia molto al chiamare un metodo per una classe OO.

Mi fermerò qui, in questa ricostruzione di una struttura orientata agli oggetti.

Quello che ho cercato di fare è dimostrare che non è troppo difficile costruire l'orientamento agli oggetti utilizzabili in un linguaggio sufficientemente potente, tra cui eredità e altre caratteristiche simili. La metacircolarità dell'interprete può aiutare, ma soprattutto a livello sintattico, che è tutt'altro che trascurabile.

I primi utenti dell'orientamento agli oggetti hanno sperimentato i concetti in questo modo. E questo è generalmente vero per molti miglioramenti ai linguaggi di programmazione. Naturalmente, anche l'analisi teorica ha un ruolo e ha aiutato a capire o affinare questi concetti.

Ma l'idea che le lingue che non dispongono di funzionalità OO sono destinate a fallire in alcuni progetti è semplicemente ingiustificata. Se necessario, possono imitare l'implementazione di queste funzionalità in modo abbastanza efficace. Molte lingue hanno il potere sintattico e semantico di orientare gli oggetti in modo abbastanza efficace, anche quando non è incorporato. E questo è più che un argomento di Turing.

OOP non affronta le limitazioni di altre lingue, ma supporta o applica metodologie di programmazione che aiutano a scrivere programmi migliori, aiutando così gli utenti meno esperti a seguire le buone pratiche che i programmatori più avanzati hanno usato e sviluppato senza quel supporto.

Credo che un buon libro per capire tutto ciò potrebbe essere Abelson & Sussman: struttura e interpretazione dei programmi per computer .


8

Un po 'di storia è in ordine, credo.

L'era dalla metà degli anni '60 alla metà degli anni '70 è oggi conosciuta come la "crisi del software". Non posso dirlo meglio di Dijkstra nella sua conferenza sul premio Turing del 1972:

La principale causa della crisi del software è che le macchine sono diventate più potenti di diversi ordini di grandezza! Per dirla senza mezzi termini: fintanto che non c'erano macchine, la programmazione non era affatto un problema; quando abbiamo avuto alcuni computer deboli, la programmazione è diventata un problema lieve e ora abbiamo computer giganteschi, la programmazione è diventata un problema altrettanto gigantesco.

Questo era il momento dei primi computer a 32 bit, i primi veri multiprocessori e i primi computer embedded, ed era chiaro ai ricercatori che questi sarebbero stati importanti per la programmazione in futuro. Era un momento nella storia in cui la domanda dei programmatori superava per la prima volta i clienti.

Non sorprende che sia stato un periodo straordinariamente fertile nella programmazione della ricerca. Prima della metà degli anni '60, avevamo LISP e AP / L, ma le lingue "principali" erano fondamentalmente procedurali: FORTRAN, ALGOL, COBOL, PL / I e così via. Dalla metà degli anni '60 alla metà degli anni '70, abbiamo ottenuto Logo, Pascal, C, Forth, Smalltalk, Prolog, ML e Modula, e questo non conta DSL come SQL e i suoi predecessori.

È stato anche un momento nella storia in cui sono state sviluppate molte delle tecniche chiave per l' implementazione dei linguaggi di programmazione. In questo periodo abbiamo ottenuto l'analisi di LR, l'analisi del flusso di dati, l'eliminazione della sottoespressione comune e il primo riconoscimento che alcuni problemi del compilatore (ad esempio l'allocazione dei registri) erano NP-difficili e cercavamo di affrontarli come tali.

Questo è il contesto in cui è nata OOP. Quindi i primi anni '70 rispondono alla tua domanda su quali problemi OOP risolva in pratica, la prima risposta è che sembrava risolvere molti dei problemi (sia contemporanei che previsti) che stavano affrontando i programmatori in quel periodo storico. Tuttavia, questo non è il momento in cui OO è diventato mainstream. Ci arriveremo presto.

Quando Alan Kay ha coniato il termine "orientato agli oggetti", l'immagine che aveva in mente era che i sistemi software sarebbero stati strutturati come un sistema biologico. Avresti qualcosa come singole celle ("oggetti") che interagiscono tra loro inviando qualcosa di analogo ai segnali chimici ("messaggi"). Non si poteva (o almeno non si scrutava) all'interno di una cella; interagiresti solo con esso attraverso i percorsi di segnalazione. Inoltre, se necessario, potresti avere più di un tipo di cella.

Puoi vedere che ci sono alcuni temi importanti qui: il concetto di un protocollo di segnalazione ben definito (nella terminologia moderna, un'interfaccia), il concetto di nascondere l'implementazione dall'esterno (nella terminologia moderna, la privacy) e il concetto di avere più "cose" dello stesso tipo in giro contemporaneamente (nella terminologia moderna, istanziazione).

Una cosa che potresti notare è mancante, ed è l'eredità, e c'è una ragione per questo.

La programmazione orientata agli oggetti è una nozione astratta e la nozione astratta può essere implementata in diversi modi in diversi linguaggi di programmazione. Il concetto astratto di un "metodo", per esempio, potrebbe essere implementato in C usando i puntatori a funzione, e in C ++ usando le funzioni membro, e in Smalltalk usando i metodi (il che dovrebbe essere sorprendente, dal momento che Smalltalk implementa il concetto astratto praticamente direttamente). Questo è ciò che le persone intendono quando indicano (giustamente) che puoi "fare" OOP in (quasi) qualsiasi lingua.

L'ereditarietà, d'altra parte, è una caratteristica concreta del linguaggio di programmazione. L'ereditarietà può essere utile per l'implementazione di sistemi OOP. O almeno, questo era il caso fino ai primi anni '90.

Il periodo compreso tra la metà degli anni '80 e la metà degli anni '90 è stato anche un periodo storico in cui le cose stavano cambiando. Durante questo periodo, abbiamo avuto l'ascesa del computer economico e onnipresente a 32 bit, quindi le aziende e molte case potevano permettersi di mettere su ogni scrivania un computer che era quasi potente come il mainframe di fascia più bassa della giornata. Era anche il periodo di massimo splendore di Questa era anche l'era dell'ascesa della moderna GUI e del sistema operativo in rete.

Fu in questo contesto che nacquero Analisi e progettazione orientate agli oggetti.

L'influenza di OOAD, il lavoro dei "tre Amigos" (Booch, Rumbar e Jacobson) e altri (ad esempio il metodo Shlaer-Mellor, il design guidato dalla responsabilità, ecc.), Non può essere minimizzato. È il motivo per cui la maggior parte dei nuovi linguaggi che sono stati sviluppati dall'inizio degli anni '90 (almeno, la maggior parte di quelli di cui hai sentito parlare) hanno oggetti in stile Simula.

Quindi la risposta degli anni '90 alla tua domanda è che supporta la migliore (al momento) soluzione per l'analisi orientata al dominio e la metodologia di progettazione.

Da allora, poiché abbiamo avuto un martello, abbiamo applicato OOP praticamente a tutti i problemi che si sono presentati da allora. OOAD e il modello a oggetti utilizzato incoraggiavano e consentivano lo sviluppo agile e guidato dai test, il cluster e altri sistemi distribuiti e così via.

Le moderne GUI e tutti i sistemi operativi progettati negli ultimi 20 anni tendono a fornire i suoi servizi come oggetti, quindi ogni nuovo linguaggio di programmazione pratico ha bisogno, almeno, di un modo per legarsi ai sistemi che usiamo oggi.

Quindi la risposta moderna è: risolve il problema di interfacciarsi con il mondo moderno. Il mondo moderno è costruito su OOP per lo stesso motivo per cui il mondo del 1880 è stato costruito su vapore: lo capiamo, possiamo controllarlo e fa il lavoro abbastanza bene.

Questo non vuol dire che la ricerca si ferma qui, ovviamente, ma indica fortemente che qualsiasi nuova tecnologia avrà bisogno di OO come caso limitante. Non devi essere OO, ma non puoi essere sostanzialmente incompatibile con esso.


Un aspetto a parte che non volevo inserire nel saggio principale è che le GUI di WIMP e OOP sembrano adattarsi in modo estremamente naturale. Si possono dire molte cose cattive delle gerarchie di eredità profonde, ma questa è una situazione (probabilmente l'UNICA situazione) in cui sembra avere un qualche senso.
Pseudonimo del

1
OOP è apparso per primo in Simula-67 (simulazione), nell'organizzazione interna dei sistemi operativi (l'idea di "classe di dispositivi" in Unix è essenzialmente una classe da cui ereditano i driver). "Sui criteri da utilizzare nei sistemi di decomposizione in moduli" di Parnas , CACM 15:12 (1972), pp. 1052-1058, linguaggio Modula di Wirth degli anni Settanta, "i tipi di dati astratti" sono tutti precursori in un modo o nel altro.
vonbrand,

È vero, ma sostengo che OOP non è stato visto come una "soluzione a un problema di programmazione procedurale" fino alla metà degli anni '70. Definire "OOP" è notoriamente difficile. L'uso originale di Alan Kay del termine non concorda con il modello di Simula, e sfortunatamente il mondo si è standardizzato sul modello di Simula. Alcuni modelli di oggetti hanno un'interpretazione simile a Curry-Howard, ma quella di Simula no. Probabilmente Stepanov aveva ragione quando ha notato che l'eredità non è corretta.
Pseudonimo,

6

Nessuno, davvero. OOP non risolve davvero un problema, a rigor di termini; non c'è niente che tu possa fare con un sistema orientato agli oggetti che non potresti fare con un sistema non orientato agli oggetti, anzi, non c'è niente che tu possa fare con nessuno dei due che non possa essere fatto con una macchina Turing. Alla fine si trasforma tutto in codice macchina e ASM non è certamente orientato agli oggetti.

Ciò che il paradigma OOP fa per te è che semplifica l'organizzazione di variabili e funzioni e ti consente di spostarli insieme più facilmente.

Di 'che voglio scrivere un gioco di carte in Python. Come dovrei rappresentare le carte?

Se non sapessi di OOP, potrei farlo in questo modo:

cards=["1S","2S","3S","4S","5S","6S","7S","8S","9S","10S","JS","QS","KS","1H","2H",...,"10C","JC","QC","KC"]

Probabilmente scriverei un codice per generare quelle carte invece di scriverle a mano, ma ottieni il punto. "1S" rappresenta il 1 di Spades, "JD" rappresenta il Jack of Diamonds e così via. Avrei anche bisogno di un codice per il Joker, ma faremo solo finta che non ci sia Joker per ora.

Ora, quando voglio mescolare il mazzo, ho solo bisogno di "mescolare" la lista. Quindi, per prendere una carta dalla cima del mazzo, apro la prima voce della lista, dandomi la stringa. Semplice.

Ora, se voglio capire con quale carta sto lavorando per mostrarla al giocatore, avrei bisogno di una funzione come questa:

def card_code_to_name(code):
    suit=code[1]

    if suit=="S":
        suit="Spades"
    elif suit=="H"
        suit="Hearts"
    elif suit=="D"
        suit="Diamonds"
    elif suit=="C"
        suit="Clubs"

    value=code[0]

    if value=="J":
        value="Jack"
    elif value="Q":
        value="Queen"
    elif value="K"
        value="King"

    return value+" of "+suit

Un po 'grande, lungo e inefficiente, ma funziona (ed è molto poco realistico, ma non è questo il punto qui).

E se volessi che le carte potessero muoversi sullo schermo? Devo conservare la loro posizione in qualche modo. Potrei aggiungerlo alla fine del loro codice della carta, ma potrebbe essere un po 'ingombrante. Invece, facciamo un altro elenco di dove si trova ogni carta:

cardpositions=( (1,1), (2,1), (3,1) ...)

Quindi scrivo il mio codice in modo che l'indice della posizione di ciascuna carta nell'elenco sia uguale all'indice della carta stessa nel mazzo.

O almeno dovrebbe essere. A meno che non commetta un errore. Cosa che potrei benissimo, perché il mio codice dovrà essere piuttosto complesso per gestire questa configurazione. Quando voglio mescolare le carte, devo mescolare le posizioni nello stesso ordine. Cosa succede se prendo completamente una carta dal mazzo? Dovrò prendere anche la sua posizione e metterla altrove.

E se volessi conservare ancora più informazioni sulle carte? Cosa succede se desidero memorizzare se ogni carta viene girata? E se volessi un motore fisico di qualche tipo e dovessi conoscere anche la velocità delle carte? Avrò bisogno di un altro elenco per memorizzare la grafica di ogni scheda! E per tutti questi punti di dati, avrò bisogno di un codice separato per mantenerli tutti organizzati correttamente in modo che ogni carta sia in qualche modo associata a tutti i suoi dati!

Ora proviamo questo nel modo OOP.

Invece di un elenco di codici, definiamo una classe Card e costruiamo da essa un elenco di oggetti Card.

class Card:

    def __init__(self,value,suit,pos,sprite,flipped=False):
        self.value=value
        self.suit=suit
        self.pos=pos
        self.sprite=sprite
        self.flipped=flipped

    def __str__(self):
        return self.value+" of "+self.suit

    def flip(self):
        if self.flipped:
            self.flipped=False
            self.sprite=load_card_sprite(value, suit)
        else:
            self.flipped=True
            self.sprite=load_card_back_sprite()

deck=[]
for suit in ("Spades","Hearts","Diamonds","Clubs"):
    for value in ("1","2","3","4","5","6","7","8","9","10","Jack","Queen","King"):
        sprite=load_card_sprite(value, suit)
        thecard=Card(value,suit,(0,0),sprite)
        deck.append(thecard)

Ora, improvvisamente, tutto è molto più semplice. Se voglio spostare la carta, non devo capire dove si trova nel mazzo, quindi usarla per ottenere la sua posizione dall'array di posizioni. Devo solo dire thecard.pos=newpos. Quando prendo la carta dall'elenco del mazzo principale, non devo creare un nuovo elenco per memorizzare tutti gli altri dati; quando l'oggetto carta si muove, tutte le sue proprietà si muovono con esso. E se voglio una carta che si comporta diversamente quando viene girata, non devo modificare la funzione di rotazione nel mio codice principale in modo che rilevi queste carte e faccia quel comportamento diverso; Devo solo sottoclassare Card e modificare la funzione flip () sulla sottoclasse.

Ma nulla di ciò che ho fatto lì non avrebbe potuto essere fatto senza OO. È solo che con un linguaggio orientato agli oggetti, il linguaggio sta facendo molto del lavoro per tenere insieme le cose per te, il che significa che hai molte meno possibilità di commettere errori e il tuo codice è più breve e più facile da leggere e scrivere.

Oppure, per riassumere ancora di più, OO ti consente di scrivere programmi apparentemente più semplici che fanno lo stesso lavoro di programmi più complessi nascondendo molte delle complessità comuni della gestione dei dati dietro un velo di astrazione.


1
Se l'unica cosa che togli di OOP è la "gestione della memoria", non penso che tu l'abbia capito molto bene. C'è un'intera filosofia di design e una generosa bottiglia di "correzione per design" lì! Inoltre, la gestione della memoria non è certamente nulla inerente all'orientamento agli oggetti (C ++?), Anche se la necessità diventa più pronunciata.
Raffaello

Certo, ma questa è la versione di una frase. E ho usato il termine anche in un modo piuttosto non standard. Forse sarebbe meglio dire "gestione delle informazioni" che "gestione della memoria".
Schilcote,

Esistono linguaggi non OOP che consentirebbero a una funzione di prendere un puntatore a qualcosa, nonché un puntatore a una funzione il cui primo parametro è un puntatore a quello stesso tipo di cose e che il compilatore convalidi che la funzione è appropriata per il puntatore passato?
supercat,

3

Avendo scritto C incorporato per alcuni anni gestendo cose come dispositivi, porte seriali e pacchetti di comunicazione tra porte seriali, porte di rete e server; Mi sono trovato, un ingegnere elettrotecnico addestrato con un'esperienza di programmazione procedurale limitata, inventando le mie stesse astrazioni dall'hardware che alla fine si è manifestato in quello che in seguito ho realizzato sono ciò che la gente normale chiama "Programmazione orientata agli oggetti".

Quando mi sono trasferito sul lato server, sono stato esposto a una factory che ha impostato la rappresentazione dell'oggetto di ciascun dispositivo nella memoria durante l'istanza. All'inizio non capivo le parole o quello che stava succedendo: sapevo solo di essere andato al file chiamato così e così e ho scritto il codice. Più tardi, mi sono ritrovato, di nuovo, a riconoscere finalmente il valore dell'OOP.

Personalmente, penso che questo sia l'unico modo per insegnare l'orientamento agli oggetti. Ho fatto un corso di introduzione a OOP (Java) il mio primo anno ed era completamente sopra la mia testa. Descrizioni OOP integrate classificazione gattino-> gatto-> mammifero-> vivo-> cosa o foglia-> ramo-> albero-> giardino sono, secondo la mia modesta opinione, metodologie assolutamente ridicole perché nessuno tenterà mai di risolverle problemi, se potessi persino chiamarli problemi ...

Penso che sia più facile rispondere alla tua domanda se la guardi in termini meno assoluti, non "cosa risolve", ma più dal punto di vista di "ecco un problema, ed ecco come lo rende più facile". Nel mio particolare caso di porte seriali, avevamo un sacco di #ifdefs in fase di compilazione che aggiungevano e rimuovevano il codice che apriva e chiudeva staticamente le porte seriali. Le funzioni di apertura della porta venivano chiamate ovunque, e potevano essere localizzate ovunque nelle 100k righe di codice OS che avevamo, e l'IDE non rendeva in grigio ciò che non era definito: dovevi rintracciarlo manualmente, e portalo nella tua testa. Inevitabilmente potresti avere diverse attività che provano ad aprire una determinata porta seriale in attesa del loro dispositivo dall'altra parte, e quindi nessuno del codice che hai appena scritto funziona, e non puoi capire perché.

L'astrazione era, sebbene fosse ancora in C, una "classe" di porta seriale (beh, solo un tipo di dati di struttura) che avevamo un array di-- uno per ogni porta seriale-- e invece di avere [l'equivalente DMA nelle porte seriali] "OpenSerialPortA" "SetBaudRate" ecc. Richiamate direttamente sull'hardware dall'attività, abbiamo chiamato una funzione di supporto a cui hai passato tutti i parametri di comunicazione (baud, parità, ecc.), Che per prima cosa ha verificato l'array di strutture per vedere se la porta era già stata aperta - in tal caso, da quale attività, che ti avrebbe indicato come debug printf, in modo da poter passare immediatamente alla sezione di codice che era necessario disabilitare-- e in caso contrario, ha continuato a impostare tutti i parametri tramite le loro funzioni di assemblaggio HAL e infine aperto la porta.

Naturalmente, ci sono anche pericoli per OOP. Quando finalmente ho ripulito quella base di codice e ho reso tutto pulito e ordinato-- scrivere nuovi driver per quella linea di prodotti era finalmente una scienza calcolabile e prevedibile, il mio manager ha eliminato il prodotto specificamente perché era un progetto in meno di cui avrebbe avuto bisogno da gestire, ed era un middle management rimovibile borderline. Il che mi ha preso molto / ho trovato molto scoraggiante, quindi ho lasciato il mio lavoro. LOL.


1
Ciao! Sembra più una storia personale che una risposta alla domanda. Dal tuo esempio, vedo che hai riscritto un codice orribile in uno stile orientato agli oggetti, che lo ha reso migliore. Ma non è chiaro se il miglioramento abbia avuto molto a che fare con l'orientamento agli oggetti o se fosse solo perché a quel tempo eri un programmatore più abile. Ad esempio, una buona parte del tuo problema sembra essere dovuta al fatto che il codice è sparso volenti o nolenti sul luogo. Ciò avrebbe potuto essere risolto scrivendo una biblioteca procedurale, senza oggetti.
David Richerby,

2
@DavidRicherby avevamo la libreria procedurale, ma questo è ciò che abbiamo deprecato, non si trattava solo di codice dappertutto. Il punto era che lo facevamo al contrario. Nessuno stava cercando di OOP nulla, è accaduto in modo naturale.
paIncrease

@DavidRicherby puoi fare qualche esempio di implementazione della libreria procedurale in modo che io possa assicurarmi che stiamo parlando della stessa cosa?
paIncrease

2
Grazie per la tua risposta e +1. Molto tempo fa un altro programmatore esperto condiviso come OOP ha fatto il suo progetto più affidabile forums.devshed.com/programming-42/... OOP Credo che è stato progettato molto intelligente da parte di alcuni professionisti che potrebbero hanno dovuto affrontare alcuni problemi di approccio procedurale.
user31782

2

ci sono molte affermazioni e intenzioni su cosa / dove la programmazione OOP ha un vantaggio rispetto alla programmazione procedurale, inclusi i suoi inventori e utenti. ma semplicemente perché una tecnologia è stata progettata per un determinato scopo dai suoi progettisti non garantisce che riuscirà a raggiungere tali obiettivi. questa è una comprensione chiave nel campo dell'ingegneria del software risalente al famoso saggio di Brooks "No silver bullet", che è ancora rilevante nonostante la rivoluzione della codifica OOP. (vedi anche il Gartner Hype Cycle per le nuove tecnologie.)

molti che hanno usato entrambi hanno anche opinioni per esperienza aneddotica, e questo ha un certo valore, ma è noto in molti studi scientifici che l'analisi auto-segnalata può essere inaccurata. sembra che ci sia poca analisi quantitativa di queste differenze o, se esiste, non è citata così tanto. è in qualche modo sorprendente il numero di scienziati informatici che parlano in modo autorevole su determinati argomenti centrali nel loro campo, ma in realtà non citano la ricerca scientifica per sostenere le loro opinioni e non si rendono conto che stanno effettivamente tramandando la saggezza convenzionale nel loro campo (sebbene diffusa ).

dato che si tratta di un sito / forum scientifico , ecco un breve tentativo di mettere le numerose opinioni su basi più solide e quantificare la differenza effettiva. potrebbero esserci altri studi e sperare che altri possano segnalarli se ne sentono parlare.(domanda zen: se c'è davvero una grande differenza, e sono stati applicati / investiti così tanti sforzi enormi nel campo dell'ingegneria del software commerciale e altrove per realizzarla, perché dovrebbe essere così difficile trovare prove scientifiche di ciò? qualche riferimento classico e molto citato nel campo che quantifica definitivamente la differenza?)

questo documento utilizza analisi sperimentali / quantitative / scientifiche e sostiene specificamente che la comprensione da parte dei programmatori alle prime armi è migliorata con i metodi di codifica OOP in alcuni casi, ma era inconcludente in altri casi (rispetto alle dimensioni del programma). nota che questa è solo una delle molte / maggiori affermazioni sulla superiorità OOP avanzata in altre risposte e dai sostenitori di OOP. lo studio stava probabilmente misurando un elemento psicologico noto come "carico cognitivo / sovraccarico" e comprensione del codice.

  • Un confronto tra la comprensione di programmi orientati agli oggetti e procedurali da parte di programmatori principianti che interagiscono con i computer. Susan Wiedenbeck, Vennila Ramalingam, Suseela Sarasamma, Cynthia L Corritore (1999)

    Questo articolo riporta due esperimenti che confrontano le rappresentazioni mentali e la comprensione del programma da parte dei novizi negli stili orientati agli oggetti e procedurali. Le materie erano programmatori alle prime armi iscritti a un secondo corso di programmazione che insegnava il paradigma orientato agli oggetti o procedurale. Il primo esperimento ha confrontato le rappresentazioni mentali e la comprensione di brevi programmi scritti negli stili procedurali e orientati agli oggetti. Il secondo esperimento ha esteso lo studio a un programma più ampio che incorpora funzionalità linguistiche più avanzate. Per i programmi brevi non vi era alcuna differenza significativa tra i due gruppi rispetto al numero totale di domande a cui era stata data una risposta corretta, ma i soggetti orientati agli oggetti erano superiori a quelli procedurali nel rispondere a domande sulla funzione del programma. Ciò suggerisce che le informazioni sulla funzione erano più facilmente disponibili nelle loro rappresentazioni mentali dei programmi e supporta un argomento secondo cui la notazione orientata agli oggetti evidenzia la funzione a livello della singola classe. Per il programma lungo non è stato trovato un effetto corrispondente. La comprensione delle materie procedurali era superiore alle materie orientate agli oggetti su tutti i tipi di domanda. Le difficoltà incontrate dai soggetti orientati agli oggetti nel rispondere alle domande in un programma più ampio suggeriscono di aver affrontato problemi nel marshalling delle informazioni e trarne inferenze. Suggeriamo che questo risultato potrebbe essere correlato a una curva di apprendimento più lunga per i principianti dello stile orientato agli oggetti, nonché alle caratteristiche dello stile OO e alla particolare notazione linguistica OO.

Guarda anche:


1
Ho rispetto per gli studi sperimentali. Esiste tuttavia il problema di accertare che rispondano alle domande giuste. Ci sono troppe variabili in ciò che può essere chiamato OOP, e in modi per usarlo, affinché un singolo studio sia significativo, imho. Come molte cose nella programmazione, OOP è stato creato da esperti per soddisfare le proprie esigenze . Quando si discute dell'utilità di OOP (che non ho preso come argomento del PO, che è piuttosto se affronta un difetto della programmazione procedurale), ci si può chiedere: quale caratteristica, per chi, a quale scopo? Quindi solo gli studi sul campo diventano pienamente significativi.
babou,

1
Avviso aneddoto: se un problema è di piccole dimensioni (ad es. Fino a circa 500-1000 righe di codice), OOP non fa alcuna differenza nella mia esperienza, potrebbe anche interferire dovendo preoccuparsi di cose che fanno poca differenza. Se il problema è grande e presenta una qualche forma di "pezzi intercambiabili" che devono inoltre essere aggiunti in un secondo momento (finestre in una GUI, dispositivi in ​​un sistema operativo, ...) la disciplina OOP dell'organizzazione è inevitabile. Puoi certamente programmare OOP senza il supporto del linguaggio (vedi ad esempio il kernel Linux).
vonbrand,

1

Stai attento. Leggi il classico di R. King "Il mio gatto è orientato agli oggetti" in "Concetti, database e applicazioni orientati agli oggetti" (Kim e Lochovsky, eds) (ACM, 1989). "Orientamento agli oggetti" è diventato più una parola d'ordine che un concetto chiaro.

Inoltre, ci sono molte variazioni sul tema, con poco in comune. Esistono linguaggi basati su prototipi (l'ereditarietà proviene da oggetti, non esistono classi in quanto tali) e linguaggi basati su classi. Ci sono lingue che consentono l'ereditarietà multipla, altre no. Alcune lingue hanno un'idea come le interfacce di Java (possono essere prese come una forma di eredità multipla annacquata). C'è l'idea dei mixin. L'ereditarietà può essere piuttosto rigorosa (come in C ++, non può davvero cambiare ciò che si ottiene in una sottoclasse) o gestita in modo molto libero (in Perl la sottoclasse può ridefinire quasi tutto). Alcune lingue hanno un'unica radice per ereditarietà (in genere chiamata Object, con comportamento predefinito), altre consentono al programmatore di creare più alberi. Alcuni linguaggi insistono sul fatto che "tutto è un oggetto", altri gestiscono oggetti e non oggetti, alcuni (come Java) hanno "la maggior parte sono oggetti, ma questi pochi tipi qui non lo sono". Alcune lingue insistono sul rigoroso incapsulamento dello stato negli oggetti, altre lo rendono facoltativo (privato, protetto, pubblico del C ++), altri non hanno affatto l'incapsulamento. Se osservi un linguaggio come Scheme dall'angolo retto, vedrai OOP integrato senza alcuno sforzo particolare (può definire funzioni che restituiscono funzioni che incapsulano un certo stato locale).


0

Per essere concisa, la programmazione orientata agli oggetti affronta i problemi di sicurezza dei dati presenti nella programmazione procedurale. Questo viene fatto utilizzando il concetto di incapsulare i dati, consentendo solo alle classi legittime di ereditare i dati. I modificatori di accesso facilitano il raggiungimento di questo obiettivo. Spero possa aiutare.:)


Quali sono i problemi di sicurezza dei dati presenti nella programmazione procedurale?
user31782

Nella programmazione procedurale non si può limitare l'uso di una variabile globale. Qualsiasi funzione potrebbe usare il suo valore. Tuttavia, in OOP potrei limitare l'uso di una variabile a una determinata classe da sola o solo alle classi che la ereditano, forse.
manu,

Anche nella programmazione procedurale possiamo limitare l'uso della variabile globale usando la variabile a determinate funzioni, ovvero non dichiarando alcun dato globale.
user31782

Se non lo dichiari globalmente non è una variabile globale.
Manu,

1
"Sicuro" o "Corretto" non significano nulla senza una specifica. Queste cose sono tentativi di mettere una specifica sul codice verso quell'obiettivo: Tipi, Definizioni di classe, DesignByContract, ecc. Ottieni "Sicurezza" nel senso che puoi rendere inviolabili i confini dei dati privati; supponendo che sia necessario obbedire al set di istruzioni della macchina virtuale per eseguire. L'orientamento agli oggetti non nasconderà la memoria interna da qualcuno in grado di leggere direttamente la memoria, e una cattiva progettazione del protocollo oggetto sta distribuendo segreti in base alla progettazione.
Rob,
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.