Problemi ereditari classici?

2

Continuo a vedere il seguente schema quando le persone imparano a conoscere OOP:

Problema: come posso inserire oggetti di tipi diversi ma correlati in un contenitore?

Soluzione: eredita da una classe base comune.

Nuovo problema: ho un sacco di oggetti di classe base, come posso ottenere il tipo reale?

Soluzione: ???

L'esempio di ereditarietà classica è di creare un programma di grafica che si occupa di oggetti geometrici. Abbiamo Circle s, Ellipse s e Triangle s. Tutti questi oggetti hanno una base comune Drawable .

InC++questopotrebbesembrarequalcosadeltipo:

structDrawable{virtual~Drawable(){}virtualvoiddraw()const=0;};structTriangle:Drawable{Pointp1,p2,p3;voiddraw()constoverride;voidstretch(doublexfactor,doubleyfactor);std::touple<double,double,double>getAngles()const;//...};structCircle:Drawable{doubleradius;voiddraw()constoverride;//...};structEllipse:Drawable{doubleradius;voiddraw()constoverride;//...};std::vector<Drawable*>drawables={newCircle,newTriangle,newEllipse};for(constauto&d:drawables)d->draw();

Finquituttobene.OravoglioaumentareilraggiodelmioCircleseEllipses.Perquantonesocisono4modiperfarlo:

1:Mettituttonellaclassebase.LaclassebaseottienelefunzionivirtualigetAngles,getRadius,stretchegetEccentricity.Esisteun'implementazionepredefinitaperciascunainmodoche,adesempio,getRadiusrestituiscaunvalorefittizioquandosihaachefareconunaTriangle.Dopoaverlofatto,puoifacilmenteeseguireiterazionisuDrawableseaumentareillororaggio.Questorendelaclassebaseincredibilmentegrandeelavoraresuunvector<Drawable*>diventainefficienteperchélefunzioninonfannonullaperlamaggiorpartedeglielementi.

2:usadynamic_casthack.

for(constauto&d:drawables){if(dynamic_cast<Circle*>(d))((Circle*)d)->radius++;elseif(dynamic_cast<Ellipse*>(d))((Ellipse*)d)->radius++;}

Purtroppodeviripeterequestabruttasintassiognivoltachedevifarequalcosadelgenere.Nonpuoimetterloinunafunzioneoinunamacro.OgnivoltachevieneaggiuntounnuovoDrawable,devipassareattraversol'interocodicebaseeaggiungerlodoveappropriato.Anchelaif/else-chainèabbastanzainefficiente.

3:aggiungiuntagcomeprimavariabilemembroeeseguiilcastdiconseguenza.

for(constauto&d:drawables){switch(d->tag){caseTag::Circle:((Circle*)d)->radius++;break;caseTag::Ellipse:((Ellipse*)d)->radius++;break;}}

Leggermentepiùefficientegrazieaunoswitchanzichéaif/else-chain,maesistegiàuntipoditagchemostrailtipodinamico,quindiiltagèridondante.Inoltrehalostessoproblemadi2:dalmomentocheènecessariomodificarel'interabasedicodiceognivoltachearrivaunnuovoDrawable.

4:mantieniglioggettiincontenitoriseparati.

vector<Circle>circles;vector<Triangle>triangles;vector<Ellipse>ellipses;for(auto&c:circles)c.radius++;for(auto&e:ellipses)e.radius++;

Questoèunlayoutdimemoriamoltoefficienteesiiterasolosuoggettichecontano.Èilmiopreferito,masfortunatamenterompelaprimasoluzioneinquantononsiconservanooggettidigitatiinmododiversoinunsingolocontenitore.

Hoprovatoarisolvereilproblemacreandounmodellovariadiccheinternamentehavectorpertipoeinoltrainserimentinelcontenitorecorretto.Idealmentenasconderebbecompletamenteilfattochecisonodiversicontenitorisottoilcofano.L'utilizzosarebbesimileaquesto:

MultiVector<Triangle,Circle,Ellipse>mv;mv.push_back(Triangle());mv.push_back(Circle());mv.push_back(Ellipse());for(auto&d:mv.get<Circle,Ellipse>())d.radius++;

Lasoluzionenonèperfetta,adesempioidiversitipinonmantengonoilloroordineel'implementazionedell'iterazioneècomplicata.

Hopresentatoquestoproblemaeiltentativodisoluzionealmiogruppodiricerca(universitario)ehoottenutolereazioni" Se progetti le tue classi correttamente questo non è un problema " e " La soluzione proposta è uno strumento molto specifico per risolvere un problema molto specifico ed è discutibile nel migliore dei casi aggiungere ancora più complessità a c ++ per un dettaglio così piccolo ". A mio parere, questo è un problema molto basilare che deve essere risolto e la solita soluzione di design di classe migliore cerca solo di trovare il meno male dei 4 workaround nel caso specifico senza effettivamente risolvere il problema. Il problema si presenta quasi ogni volta che viene utilizzata l'ereditarietà.

Quindi finalmente ecco la domanda: hai incontrato anche questo problema / questo è un vero problema? Sai di un'altra soluzione / soluzione? Altre lingue affrontano meglio il problema? Voglio simulare un sondaggio usando 2 commenti qui sotto, quindi sentiti libero di scegliere " Non ho mai avuto questo problema / Il problema è stato risolto facilmente. " o " Ho anche avuto questo problema e non ho mai risolto per la mia soddisfazione. "

Modifica: per evitare una chiusura dovuta principalmente a opinioni / discussioni: voglio una risposta chiara che risolva il problema o una risorsa che dice che non ce n'è.

Modifica: correlati: Ho davvero un'auto in il mio garage?

    
posta nwp 12.07.2014 - 00:58
fonte

5 risposte

4

Questo è un classico problema di OOP, non che segnali che c'è qualcosa di sbagliato nel modo di pensare orientato agli oggetti, ma richiede di considerare attentamente la progettazione.

Quando desideri eseguire iterazioni su un contenitore di oggetti e modificare alcune proprietà che solo una parte degli oggetti possiede, e, cosa più importante: ha senso solo per una parte degli oggetti , è a mio parere il design è il problema. La domanda qui è, perché vuoi modificare alcune proprietà specifiche di una classe in un elenco generico? Sono sicuro che abbia un buon senso nella tua situazione, ma pensaci per un po '.

Hai una lista di figure, che possono essere quadrati, rettangoli, triangoli, cerchi, poligoni, ecc. E ora desideri eseguire qualche azione su di loro, che va bene, se tutti supportano l'azione. Ma non ha senso modificare una proprietà su un oggetto che chiaramente non lo supporta. È contro-intuitivo iterare un elenco di figure, impostando il raggio, è non comunque contro-intuitivo per iterare un elenco di cerchi, impostando il raggio.

Questo non significa che un po 'complicato, o diciamo semplicemente una scelta di design intelligente, possa permetterti di raggiungere questo obiettivo, ma va contro il concetto di orientamento agli oggetti, cercando in qualche modo di aggirare l'incapsulamento. Direi che questo va contro l'idea del polimorfismo.

Invece come farlo è qualcosa di diverso, che temo di non poter produrre una risposta in questo momento. Credo tuttavia che dovresti fare del tuo meglio per cercare di evitare di metterti in quella situazione.

Una possibile alternativa che potresti esaminare è l'utilizzo di un modello di visitatore, in cui hai un visitatore che accetta un tipo di oggetto che consente di modificare il raggio. Ad esempio:

class Visitor {
public:
  void Visit(Circle& circle) { /* Do something circle specific */ )
  void Visit(Square& square) { /* Do something square specific */ )
  // ...
};
    
risposta data 12.07.2014 - 01:31
fonte
2
Problem: How do I put objects of different but related types into a container?

Solution: Inherit from a common base class.

Questo è il problema. Se il tuo progetto richiede tipi separati, non dovresti unirli per metterli in un contenitore. Hai scelto un esempio in cui nessuno ti guarderà in modo divertente se li unisci tutti in una classe base comune, ma questa soluzione chiaramente non funziona quando i tipi hanno meno in comune, o sono tipi primitivi / scalari o sono di terze parti tipi che non controlli.

Quello che vuoi è un unione taggata . C / C ++ non ha questi, ma ha unioni untagged che puoi taggare da soli:

enum ShapeType { TRIANGLE, CIRCLE, ELLIPSE };

struct ShapeUnion {
    ShapeType type;
    union {
       Triangle triangle;
       Circle circle;
       Ellipse ellipse;
    } data;
};

Controllando l'enum puoi determinare a quale membro del sindacato accedere:

std::vector<ShapeUnion> shapes = getShapes();
for (auto& shape: shapes) {
    switch (shape.type) {
        case TRIANGLE: foo(shape.data.triangle); break;
        case CIRCLE: bar(shape.data.circle); break;
        case ELLIPSE: baz(shape.data.ellipse); break;
        default: // raise some sort of error
    }
}

Sono sicuro che puoi trovare modelli per questo se cerchi un po 'google.

Dichiarazione di non responsabilità: non scrivo molto C / C ++, il codice sopra può contenere errori di sintassi.

    
risposta data 12.07.2014 - 04:25
fonte
1

Quindi, ecco il problema: qual è l'ereditarietà classica? Tassonomia! Gerarchie rigorose e simili. Le gerarchie funzionano sulla base della comunanza dei tipi. Quindi ora stai cercando di dire "Voglio raggruppare cose diverse dalle altre". Ok, puoi farlo. Ma dal momento che non sono dello stesso tipo, un modello di risoluzione dei problemi basato sulla somiglianza non è adatto a recitare su di loro come un gruppo ... a meno che non trovi il modo in cui sono simili. Se sono davvero simili, puoi agire come gruppo in un modo astratto creando una classe base e non dovresti dover attivare il tipo. Se non sono affatto simili, non è possibile agire su di essi senza accendere il tipo. E davvero, ammettiamolo: il polimorfismo in realtà sta facendo questo cambiando quale implementazione è chiamata sotto il cofano, quindi in qualche strano modo anch'essa è come un'istruzione switch - solo più veloce ed elegante.

Quindi, se ci pensi in questo modo, devi inserire l'interruttore nella classe stessa (ereditarietà classica, probabilmente con qualche strano metodo "DoStuff ()" o "Process ()") o nel cosa agisce sulla classe (modello di visitatore o tipi di attivazione) o segregare gli elenchi (soluzione n. 4). Il vantaggio del tipo di accesso / visitatore è che non confonde la tua classe base ed è quindi più piacevole per il codice modulare.

Ora OOP non è l'unico paradigma in città. I game designer spesso devono pensare a oggetti che non formano una gerarchia rigida, ma che hanno determinati "attributi". Potresti trovare interessante la progettazione dei componenti o le relative varianti se ti imbatti spesso in questo tipo di problema.

    
risposta data 12.07.2014 - 05:10
fonte
1

Personalmente sto perfettamente bene usando downcasting. Semplicemente perché è lo stesso del modello di visitatore supportato dalla lingua. Diventa ancora migliore se crei HasRadius interface / abstract class, che ti consente di specificare quali classi consentono questa modifica. Questo ti consente quindi di aggiungerlo a potenzialmente nuovo Drawable ed elimina la catena if. E alcuni dei reclami relativi a questa soluzione possono essere risolti implementando il visitatore effettivo, poiché le modifiche ai tipi saranno garantite per essere corrette dal sistema di tipi e dal compilatore.

E una cosa da notare: parli molto dell'efficienza. La programmazione riguarda il tradeofs e le astrazioni (specialmente quelle che aggiungono proprietà Open / Closed alla progettazione) vanno sempre direttamente contro l'efficienza. Non sto dicendo che l'efficienza non sia necessaria. Solo quando inizi a utilizzare OOP, devi essere in grado di accettare un certo impatto sulle prestazioni. E non dimenticare: dovresti lasciare le ottimizzazioni delle prestazioni dopo aver profilato il tuo codice. Pensare alle prestazioni in questo esempio sta andando a spulciare i risultati estremamente.

    
risposta data 12.07.2014 - 09:20
fonte
0

Hai chiesto:

Did you run into this problem too / is this a real problem? Do you know of another workaround/solution?

La mia risposta è un grande SÌ. Mi occupo molto di questo nel mio lavoro.

Hai detto,

Now I want to increase the radius of my Circles and Ellipses.

La chiave delle tue linee di soluzione per approfondire i dettagli di tale requisito. Non c'è "I" nel codice. È necessario identificare quali parti del codice devono eseguire tale operazione. Quella parte del tuo codice deve sapere su Circles and Ellipses. Se quella sezione del codice conosce Circles ed Ellipses, possono prendere un puntatore o riferimento di classe base ed eseguire un dynamic_cast . Se il dynamic_cast ha esito positivo, possono chiamare direttamente le funzioni membro. Se dynamic_cast non riesce, possono ignorare quell'oggetto, lanciare un'eccezione o gestirla in un altro modo.

Un approccio generico

Abbiamo trattato le proprietà generiche utilizzando chiavi basate su stringhe e valori generici. Ad esempio, un gestore di proprietà generico associato a Cerchi ed Ellissi si registrerebbe da sé indicando che sa come gestire le proprietà "raggio" degli oggetti. Un meccanismo di distribuzione generico chiamerebbe quel gestore di proprietà quando ha bisogno di impostare il "raggio" di un Drawable.

In pseudo codice, avresti:

void setProperty(Drawable& dr, std::string const& key, std::string const& value)
{
    PropertyHandler* handler = getPropertyHandler(key);
    if ( handler != NULL )
    {
       handler.setProperty(dr, key, value);
    }
}
    
risposta data 12.07.2014 - 05:58
fonte

Leggi altre domande sui tag