Struct + Funzioni che operano sugli oggetti struct vs. OOP

2

Recentemente ho iniziato a conoscere OOP e ho letto che OOP è un paradigma di programmazione diverso dalla programmazione procedurale. Ma non ho trovato nessun esempio che mostri in che modo sono diversi.

Voglio dire che sto usando OOP e ho creato una classe Car con una funzione moveForward() e poi ho chiamato questa funzione:

Car car = new Car();    // Car is a class here
car.moveForward();

Ora diciamo che sto usando la programmazione procedurale e ho creato una struttura Car e una funzione moveForward() che accetta una struct Car come argomento, quindi ho chiamato questa funzione:

Car car = new Car();    // Car is a struct here
moveForward(car);

Quindi mi sembra che OOP e la programmazione procedurale siano solo diversi nel modo in cui si chiama una funzione di un oggetto, ma il modo in cui si pensa a un problema e si implementa la sua soluzione è quasi lo stesso!

    
posta user9002947 10.03.2018 - 18:00
fonte

4 risposte

4

Come hai osservato, per il cosiddetto codice orientato agli oggetti, la differenza rispetto alla programmazione procedurale è puramente sintattica.

Ci sono molte opinioni diverse su cosa sia effettivamente la programmazione orientata agli oggetti. Un punto di vista è: gli oggetti comunicano inviando messaggi a vicenda. Quindi, dicendo "hey procedura, esegui con questi dati", diciamo "hey object, gestisci questo messaggio". È responsabilità dell'oggetto capire quale codice debba essere eseguito in risposta a un messaggio e al chiamante non è garantito che venga eseguita alcuna procedura specifica.

Rendere la funzione di spedizione la responsabilità dell'oggetto ricevente apre alcune interessanti opportunità: dobbiamo fare la differenza tra l'interfaccia pubblica promessa dagli oggetti ai suoi utenti e l'implementazione interna di questa interfaccia. Ciò consente anche l'iniezione di ereditarietà e dipendenza: possiamo creare un nuovo oggetto conforme alla stessa interfaccia (risponde ai messaggi in modo compatibile) e sostituire l'oggetto originale - senza dover cambiare il codice chiamante. Poiché i messaggi sono (supposti essere) valori, possono anche essere inviati su rete, quindi il pensiero orientato agli oggetti si presta a problemi distribuiti come i microservizi.

In pratica, la maggior parte delle lingue non usa effettivamente i messaggi e utilizza una tecnica chiamata dispatch dinamico: l'oggetto (o la classe di un oggetto) contiene una tabella di puntatori di funzione. Il layout di questa tabella costituisce l'interfaccia dell'oggetto. Quindi posso avere molti oggetti diversi conformi alla stessa interfaccia, ma implementarli in modo diverso.

Per esempio, pensiamo a una simulazione del traffico con auto e moto. Auto e moto hanno caratteristiche molto diverse ma per il nostro codice di simulazione sono entrambi partecipanti al traffico. Un approccio procedurale sarebbe quello di verificare con ifs ed elses che tipo di partecipante al traffico abbiamo:

Car_moveForward(Car*) { ... }
Bike_moveForward(Bike*) { ... }

// in using code:
for (int i = 0; i < len; i++) {
  if (participants[i]->type == TP_CAR)
    Car_moveForward((Car*) participants[i]);
  else if (participants[i]->type == TP_BIKE)
    Bike_moveForward((Bike*) participants[i]);
  else
    error("unexpected traffic participant type");
}

Ecco come scrivere un sacco di codice in C.

Con OOP, definiamo invece un'interfaccia che descrive le operazioni di cui abbiamo bisogno. Tutti gli oggetti implementano questa interfaccia:

Car::moveForward() { ... }
Bike::moveForward() { ... }

// using code:
for (int i = 0; i < len; i++) {
  // doesn't need to know which implementation to call
  participants[i]->moveForward();
}

Quindi questa interfaccia ci permette di rimuovere esplicitamente se / else controlla il tipo concreto. Questo ci dà anche molta estensibilità: se vogliamo aggiungere un altro tipo di partecipante al traffico (ad esempio i pedoni), dovremmo aggiornare ogni tipo di controllo if-else nel codice procedurale. Ma con un'interfaccia, dobbiamo solo implementare quell'interfaccia e il nostro nuovo tipo può essere già utilizzato ovunque: il codice che utilizza un'interfaccia non deve cambiare quando l'interfaccia è implementata da un altro tipo.

Molti problemi non hanno bisogno di queste interfacce. Un sacco di codice può essere felicemente procedurale con la sintassi OOP-ish. Uno dei più grandi casi d'uso per le interfacce è il disaccoppiamento e l'iniezione di dipendenza, in particolare per i test unitari. Ad esempio, un'applicazione potrebbe effettuare richieste di database. Come posso testare questo senza creare un database? Innanzitutto, introdurre un'interfaccia che descriva ciò che mi serve dal DB, quindi implementare quell'interfaccia per il DB. Ma i miei test possono utilizzare un'implementazione fittizia di questa interfaccia che restituisce solo dati statici invece dell'implementazione reale del DB.

Il libro Design Patterns contiene una raccolta di problemi in cui le tecniche orientate agli oggetti possono fornire una soluzione elegante. Sono tutti basati sull'idea di utilizzare le interfacce in modo che il codice chiamante non abbia bisogno di conoscere il tipo concreto in anticipo.

    
risposta data 10.03.2018 - 18:55
fonte
2

La cosa che ti è sfuggita è che nella programmazione procedurale se hai elementi diversi nel loro avanzamento avresti diverse funzioni per spostare elementi diversi come:

MoveForwardCar(car);
MoveForwardBike(bike);

e così via.

In OOP puoi spostare quella funzionalità nei metodi sugli oggetti in modo che il resto del codice non abbia bisogno di sapere come spostare qualcosa in avanti, deve solo sapere che può andare avanti. L'andare avanti è incapsulato dalle classi. Così fai così:

car.MoveForward();
bike.MoveForward();

Solo le classi Car e Bike sanno cosa succede quando vanno avanti.

Se aggiungi una nuova classe Ship devi solo aggiungere il metodo MoveForward alla classe nave. Il resto del codice non deve cambiare, perché quando si spostano in avanti le navi si comportano esattamente come auto e moto per il resto del codice.

    
risposta data 10.03.2018 - 18:19
fonte
1

Non sono d'accordo che la differenza tra codice procedurale e orientato agli oggetti sia puramente sintattica. È esattamente il contrario.

Non c'è quasi alcuna differenza sintattica, ad esempio Java potrebbe essere pensato come procedure con il primo argomento che è "l'oggetto" o "struttura" su cui è definito. Puoi scrivere codice procedurale molto facilmente in Java, e sfortunatamente la maggior parte dei progetti lo fa.

La vera differenza è nel " pensiero ", come ti avvicini a un problema. Gli oggetti dovrebbero essere agenti autonomi cooperanti. Contrariamente al pensiero "procedurale", in cui si pensa ai "passaggi", ad esempio: ho bisogno di aprire il file, di analizzare il CSV, aggiungere tutte le colonne e scrivere la somma sull'output. In oo penseresti: ci sono delle letture che possono generare un ReadoutReport (o qualcosa del genere).

Il pensiero è completamente diverso. Anche se può risultare in un codice a volte simile a quello che farebbe uno sviluppatore procedurale, questa somiglianza è puramente casuale.

Inoltre non ha nulla a che fare con la maggior parte delle cose tecniche che la gente menziona. Classi, eredità, modelli, DI, qualunque cosa siano le cose secondarie nella migliore delle ipotesi. Alcuni di questi sono buoni strumenti, non fraintendetemi, ma sono secondari alla natura di oo, che è di nuovo un oggetto (agenti autonomi) che collaborano per ottenere alcune funzionalità.

    
risposta data 11.03.2018 - 14:55
fonte
0

So it seems to me that OOP and procedural programming are only different in the way you call a function of an object, but the way you think about a problem and implement its solution is almost the same!

Sì, anche se solo per semplici esempi. OOP offre funzionalità che vanno oltre.

In primo luogo, OOP supporta l'astrazione di un'interfaccia: stuzzicare l'uso e la sua implementazione, accoppiandoli liberamente e quindi supportando la sostituzione.

Questo fornisce il polimorfismo, che consente un certo tipo di riutilizzo del codice: vale a dire, lo stesso client che utilizza un'interfaccia può (assumendo liskov) essere riutilizzato - significativamente e senza modifiche - con diverse implementazioni dell'interfaccia.

Questo è più difficile da fare usando uno stile di programmazione procedurale, poiché l'interfaccia e un'implementazione sono intrinsecamente strettamente accoppiate. È necessario qualcosa in più per aiutarli a disaccoppiarli, e questo includerebbe alcune tecniche OOP.

In secondo luogo, l'eredità di OOP è un meccanismo per il riutilizzo del codice di implementazione. I metodi della classe base possono essere riutilizzati specializzando le sottoclassi. (Tieni comunque presente la raccomandazione per la composizione sull'ereditarietà.)

E questi due: separazione dell'interfaccia dall'implementazione e l'ereditarietà del codice, possono lavorare insieme: una sottoclasse specializzata in grado di riutilizzare parte dell'implementazione delle classi base e il consumo del codice client può essere riutilizzato non modificato con quella sub specializzata classe.

Se scriviamo codice come questo:

Car car = new Car();    // Car is a class here
car.moveForward();

in sole due righe di codice, stiamo confondendo due ruoli diversi. Il ruolo giocato dalla seconda riga di codice è quello di un client che consuma un'interfaccia, mentre il ruolo giocato dalla prima riga di codice è quello di decisore, iniettore, fabbrica o configuratore. Spesso, vorremmo separare questi ruoli, in diversi moduli. Pertanto, la macchina potrebbe essere creata in un modulo e utilizzata da un'altra.

OOP incoraggia questa separazione, con l'aspettativa che (quando fatto correttamente) il modulo client consumante possa eseguire su molteplici potenziali implementazioni.

Struct vs. Class è una distinzione che dipende dalla lingua che stai utilizzando.

C, ovviamente, non ha classi; struct's ha una semantica a valore finché non si usano i puntatori. Una volta che usi i puntatori, puoi applicare alcune tecniche OOP.

In C ++ non c'è praticamente alcuna differenza tra struct e class (il default del modificatore di accesso è diverso, public per struct, private per le classi).

C # ha sia structs che classi in cui il primo ha semantica di valore e la seconda semantica di riferimento (ad es. variabili locali e parametri può essere e può fa riferimento agli oggetti, sebbene non possa essere oggetti).

Le classi di Java e C # sono simili; Java non ha strutture: i suoi unici tipi di valore sono le primitive (ad es. Int).

Quindi, in C #, ad esempio, supponendo che Car sia una classe, Car car; crea una variabile di riferimento che può riferirsi a un oggetto che è di tipo Car , sebbene anche qualsiasi sottoclasse di Car . Mentre se Car è una struct, allora quella stessa dichiarazione crea una struct di tipo Car .

    
risposta data 10.03.2018 - 18:46
fonte

Leggi altre domande sui tag