2010-11-12 15 views
5

Sto cercando di utilizzare la classe base astratta C++ in modo simile con l'interfaccia Java. Suppone che abbiamo seguenti classi di interfaccia con funzioni virtuali solo puri:Ereditarietà parallela tra classi di interfaccia e classi di implementazione in C++

class Shape { virtual double area()=0; }; 
class Square : public Shape { virtual void setLength(double length)=0; }; 
class Rectangle : public Square { virtual void setWidth(double width)=0; }; 

e cerco di implementare rettangolo e del quadrato seguente modo:

class SquareImpl : public Square { /*implementation*/ }; 
class RectangleImpl : public SquareImpl, Rectangle { /*implementation*/ }; 

Dove RectangleImpl eredita sia SquareImpl e Rectangle riutilizzare, diciamo , SquareImpl::area(). Tuttavia, quando provo a compilare, sorgono due problemi: in primo luogo, tutti i metodi in SquareImpl non vengono ereditati correttamente e devo reimplementare manualmente RectangleImpl::area() e RectangleImpl::setLength(). In secondo luogo, questo introduce ancora il problema dei diamanti che Shape è una base ambigua di RectangleImpl.

ho potuto compilare il codice se praticamente ereditato Square da Shape, ma non credo che le prestazioni saranno scala con interfacce più derivati ​​aggiunto. Anche stranamente, RectangleImpl non eredita ancora SquareImpl::setLength() sebbene SquareImpl::area() sia ereditato bene. (ignorare la praticità qui)

Un'altra soluzione potrebbe essere quella di rendere le interfacce indipendenti l'una dall'altra, vale a dire rendere Square non ereditato da Shape. Ma farlo mi farà perdere l'accesso ai metodi in Shape se definisco funzioni che prendono un puntatore Square*. Renderà anche static_cast impossibile tra Shape e Square.

Quindi la mia domanda è: esiste un altro modello di progettazione in C++ per risolvere questo tipo di ereditarietà parallela tra classi di interfaccia e classi di implementazione, senza richiedere l'ereditarietà virtuale?

(modifica precisazione: il codice di esempio di cui sopra sono solo la mia illustrazione manichino su eredità parallelo tra le interfacce e le implementazioni Capisco che ci sono modi migliori per attuare forme ma il mio problema non è su come implementare le forme..)

+0

"Dove RectangleImpl eredita sia SquareImpl e Rettangolo di riutilizzare, ad esempio, SquareImpl :: area()" => ereditano per essere riutilizzati, non riutilizzare. – icecrime

+0

dupe di http://stackoverflow.com/questions/249500/looking-for-a-better-way-than-virtual-inheritance-in-c? –

+0

È prevista l'ereditarietà non pubblica di Rectangle in RectangleImpl? – daramarak

risposta

3

Quello che hai qui è il caso dello Diamond Problem, che può verificarsi in qualsiasi linguaggio OO che consente l'ereditarietà multipla. Questo, tra l'altro, è una delle ragioni per cui i progettisti di Java hanno deciso di non avere ereditarietà multipla e hanno sviluppato la nozione di interfaccia.

Il modo in cui il C++ si occupa del problema dei diamanti è Virtual Inheritance.

E, come indicato da codymanix, il quadrato e il rettangolo sono notoriamente un cattivo esempio per la progettazione orientata agli oggetti, perché per quanto riguarda l'OO è a square is not a rectangle.

Coppia più punti. Innanzitutto, il termine per ciò che stai facendo qui è ereditarietà multipla, non "ereditarietà parallela". In secondo luogo, in questo caso particolare ha davvero poco senso avere un class Square e un class SquareImpl. Se pensi che potresti avere implementazioni diverse di Square, dovresti semplicemente avere una classe base che fornisce un'implementazione predefinita e funzioni virtuali che possono essere sostituite da una classe derivata, se necessario. In altre parole, è necessario caricare Square e SquareImpl in una classe con funzioni virtuali.

È sicuramente possibile utilizzare una classe C++ astratta come un'interfaccia Java, ma il più delle volte non c'è motivo. Le interfacce sono state aggiunte a Java proprio come un modo per aggirare la mancanza di ereditarietà multipla. In C++ puoi semplicemente andare avanti e usare l'ereditarietà multipla, anche se dovresti farlo sempre molto giudiziosamente.

+1

Ho usato le forme come esempio di implementazione di qualcosa che è potenzialmente complesso dietro una classe di interfaccia C++. Mentre in questo caso 'Square' è abbastanza semplice da avere l'implementazione e l'interfaccia combinate, potrebbe non essere il caso di altre situazioni. Comunque grazie per la nota che Shape è un notoriamente cattivo esempio. Credo che userò FooBar nei miei esempi la prossima volta. – Soares

+0

Il tuo suggerimento di eseguire Square e SquareImpl in una singola classe funziona in questo caso, ma il problema di Soares si verifica frequentemente durante la scrittura del codice per l'integrazione delle dipendenze. Per DI, avrai sempre 2 classi SquareImpl che hanno implementazioni molto diverse (una è reale, una è un semplice oggetto fittizio). C'è zero codice da condividere, quindi ha perfettamente senso farli ereditare dalla stessa interfaccia. Per mia informazione, la soluzione che ho seguito per DI è quella di fare semplicemente l'ereditarietà virtuale dallo stack di ereditarietà dell'interfaccia. Un po 'di duplicazione, ma almeno funziona: / – Weston

0

Square non è Rectangle e Rectangle non Square. L'unica cosa che hanno in comune è che sono forme. Quindi:

class Square : public Shape {...}; 
class Rectangle : public Shape {...}; 

Le loro funzioni di inizializzazione sono diversi, e Square::setSide(double)Rectangle::setLengthAndWidth(double, double). Non hai bisogno * di lezioni di Impl. Fai le tue cose in Square e Rectangle.

+0

Il mio esempio è solo un'illustrazione del problema. Capisco che ci sono modi migliori per implementare la forma, ma ho appena creato le classi per mostrare il mio punto di ereditarietà parallela. – Soares

+1

Ah, illustrazione sbagliata. Hmm, penso che dovresti rimuovere 'public SquareImpl' da' class RectangleImpl', e inserire il codice comune in alcune funzioni statiche che sia 'SquareImpl' che 'RectangleImpl' possono chiamare. – Dialecticus

+1

-1 Sento che non hai davvero risposto al problema degli askers, penso che sia chiaro che ottiene le basi qui – Elemental

0

Penso che dovresti cercare l'ereditarietà virtuale in modo che tu abbia solo una singola istanza di forma al di sotto di tutto questo - questo sembra ovvio da un punto di vista semantico - cioè la forma che quadrato e rettangoloimpl è chiaramente la stessa forma .

Non proprio sicuro del problema di prestazioni che hai citato, non mi sembra un problema (nel senso che non è un overhead extra se non quello di chiamare qualsiasi funzione v). Non vedo perché non si può accedere a setLength da square - difficile capire perché si sta verificando questo e non si ha origine per le implementazioni.

0

Il tuo problema è Rectangle-> Square-> Shape non conosce nulla di SquareImpl, quindi non può utilizzare queste funzioni per soddisfare i suoi requisiti di funzione astratta.

Il modo più semplice è non mischiare l'interfaccia e l'ereditarietà dell'implementazione quando sono strettamente legati in questo modo. Rendi RectangleImpl ereditato dalle interfacce Square e Rectangle. Se SquareImpl e RectangleImpl stanno replicando troppo il codice degli altri, usa una classe che fa tutto questo e che ha come funzione membro in ogni implementazione.

0

Dopo il ripensamento per una notte e facendo riferimento al solution sean fornito allo Looking for a better way than virtual inheritance in C++, sono uscito con la seguente soluzione.

Qui ridefinisco il problema per essere più astratto per evitare la confusione che avevamo sulle forme. Abbiamo un'interfaccia Ball che può rotolare, un'interfaccia FooBall che contiene metodi Foo specifici e un'interfaccia FooBarBall anch'essa un FooBall e che contiene sia metodi specifici specifici che metodi specifici. Come il problema originale, abbiamo un'implementazione FooBall e desideriamo derivarla per coprire anche i metodi specifici della barra. ma ereditando sia l'interfaccia che l'implementazione introdurrà l'ereditarietà del diamante.

Per risolvere il problema, invece di mettere direttamente foo e bar metodi specifici nei derivati ​​Ball interfacce, metto un unico metodo in un derivato FooBall interfaccia che converte l'oggetto in un oggetto Foo attraverso il metodo toFoo(). In questo modo, le implementazioni possono combinarsi nell'interfaccia indipendente Foo e Bar senza introdurre l'ereditarietà del diamante.

Ancora, non tutti i codici possono essere eliminati per ricavare liberamente tutte le barre da Foos. Dobbiamo ancora scrivere implementazioni indipendenti di Ball, FooBall e FooBarBall che non ereditano gli uni dagli altri.Ma possiamo usare il pattern composito per avvolgere gli oggetti reali Foo e Bar implementati in modo diverso. In questo modo possiamo ancora eliminare un bel po 'di codice se abbiamo molte implementazioni di Foo e Bar.

#include <stdio.h> 

class Ball { 
    public: 
    // All balls can roll. 
    virtual void roll() = 0; 

    // Ball has many other methods that are not 
    // covered here. 

    virtual inline ~Ball() { 
     printf("deleting Ball\n"); 
    }; 
}; 

class Foo { 
    public: 
    virtual void doFoo() = 0; 

    // do some very complicated stuff. 
    virtual void complexFoo() = 0; 

    virtual inline ~Foo() {}; 
}; 

/** 
* We assume that classes that implement Bar also 
* implement the Foo interface. The Bar interface 
* specification failed to enforce this constraint 
* by inheriting from Foo because it will introduce 
* diamond inheritance into the implementation. 
**/ 
class Bar { 
    public: 
    virtual void doBar() = 0; 
    virtual void complicatedBar() = 0; 

    virtual inline ~Bar() {}; 
}; 

class FooBall : public Ball { 
    public: 
    virtual Foo* toFoo() = 0; 

    virtual inline ~FooBall() {}; 
}; 

/** 
* A BarBall is always also a FooBall and support 
* both Foo and Bar methods. 
**/ 
class FooBarBall : public FooBall { 
    public: 
    virtual Bar* toBar() = 0; 

    virtual inline ~FooBarBall() {}; 
}; 


/* Composite Implementation */ 

class FooImpl_A : public Foo { 
    public: 
    virtual void doFoo() { 
     printf("FooImpl_A::doFoo()\n"); 
    }; 

    virtual void complexFoo() { 
     printf("FooImpl_A::complexFoo()\n"); 
    } 

    virtual inline ~FooImpl_A() { 
     printf("deleting FooImpl_A\n"); 
    } 
}; 

class FooBarImpl_A : public FooImpl_A, public Bar { 
    public: 
    virtual void doBar() { 
     printf("BarImpl_A::doBar()\n"); 
    } 

    virtual void complicatedBar() {; 
     printf("BarImpl_A::complicatedBar()\n"); 
    } 

    virtual inline ~FooBarImpl_A() { 
     printf("deleting FooBarImpl_A\n"); 
    } 
}; 

/* Composite Pattern */ 
class FooBarBallContainer : public FooBarBall { 
    public: 

    /* FooBarBallImpl_A can take any class that 
    * implements both the Foo and Bar interface, 
    * including classes that inherit FooBarImpl_A 
    * and other different implementations. 
    * 
    * We'll assume that realFoo and realBar are 
    * actually the same object as Foo methods have 
    * side effect on Bar methods. If they are not 
    * the same object, a third argument with false 
    * value need to be supplied. 
    */ 
    FooBarBallContainer(Foo* realFoo, Bar* realBar, bool sameObject=true) : 
    _realFoo(realFoo), _realBar(realBar), _sameObject(sameObject) {} 

    virtual void roll() { 
     // roll makes use of FooBar methods 
     _realBar->doBar(); 
     _realFoo->complexFoo(); 
    } 

    virtual Foo* toFoo() { 
     return _realFoo; 
    } 

    virtual Bar* toBar() { 
     return _realBar; 
    } 

    virtual ~FooBarBallContainer() { 
     delete _realFoo; 

     // Check if realFoo and realBar are 
     // not the same object to avoid deleting 
     // it twice. 
     if(!_sameObject) { 
      delete _realBar; 
     } 
    } 

    private: 
    Foo* _realFoo; 
    Bar* _realBar; 
    bool _sameObject; 
}; 


/* Monolithic Implmentation */ 

class FooBarBallImpl_B : public FooBarBall, 
    public Foo, public Bar { 

    public: 
    virtual void roll() { 
     complicatedBar(); 
     doFoo(); 
    } 

    virtual Foo* toFoo() { 
     return (Foo*) this; 
    } 

    virtual Bar* toBar() { 
     return (Bar*) this; 
    } 

    virtual void doFoo() { 
     printf("FooBarBallImpl_B::doFoo()\n"); 
    } 

    virtual void complexFoo() { 
     printf("FooBarBallImpl_B::complexFoo()\n"); 
    } 

    virtual void doBar() { 
     printf("FooBarBallImpl_B::doBar()\n"); 
    } 

    virtual void complicatedBar() { 
     printf("FooBarBallImpl_B::complicatedBar()\n"); 
    } 

}; 

/* Example usage of FooBarBall */ 
void processFooBarBall(FooBarBall *ball) { 

    Foo *foo = ball->toFoo(); 
    foo->doFoo(); 

    ball->roll(); 

    Bar *bar = ball->toBar(); 
    bar->complicatedBar(); 
} 

main() { 

    FooBarImpl_A *fooBar = new FooBarImpl_A(); 
    FooBarBall *container = new FooBarBallContainer(fooBar, fooBar); 

    printf 
    processFooBarBall(container); 
    delete container; 

    FooBarBallImpl_B *ball = new FooBarBallImpl_B(); 
    processFooBarBall(ball); 

    // we can even wrap FooBarBallImpl_B into the container 
    // but the behavior of roll() will become different 
    container = new FooBarBallContainer(ball, ball); 
    processFooBarBall(container); 

    delete container; 

} 
Problemi correlati