Domanda

OK, ho un sistema alquanto complicato in C ++. In poche parole, devo aggiungere un metodo a una classe base astratta di terze parti. La terza parte fornisce anche un sacco di classi derivate che necessitano anche della nuova funzionalità.

Sto usando una libreria che fornisce un'interfaccia Shape standard, nonché alcune forme comuni.

class Shape
{
    public:
        Shape(position);
        virtual ~Shape();

        virtual position GetPosition() const;
        virtual void SetPosition(position);

        virtual double GetPerimeter() const = 0;

    private: ...
};

class Square : public Shape
{
    public:
        Square(position, side_length);
    ...
};

class Circle, Rectangle, Hexagon, etc

Ora, ecco il mio problema. Voglio che la classe Shape includa anche una funzione GetArea (). Quindi sembra che dovrei semplicemente fare un:

class ImprovedShape : public virtual Shape
{
    virtual double GetArea() const = 0;
};

class ImprovedSquare : public Square, public ImprovedShape
{
    ...
}

E poi vado a creare un ImprovedSquare che eredita da ImprovedShape e Square. Bene, come puoi vedere, ora ho creato il temuto problema di eredità del diamante . Ciò sarebbe facilmente risolvibile se la biblioteca di terze parti usasse eredità virtuale per il loro Square, Circle, ecc. Tuttavia, farli fare non è un'opzione ragionevole.

Quindi, cosa fai quando devi aggiungere un po 'di funzionalità a un'interfaccia definita in una libreria? C'è una buona risposta?

Grazie!

È stato utile?

Soluzione

Abbiamo avuto un problema molto simile in un progetto e lo abbiamo risolto semplicemente NON derivando ImprovedShape da Shape. Se hai bisogno della funzionalità Shape in ImprovedShape puoi dynamic_cast, sapendo che il tuo cast funzionerà sempre. E il resto è proprio come nel tuo esempio.

Altri suggerimenti

Perché questa classe deve derivare dalla forma?

class ImprovedShape : public virtual Shape
{
    virtual double GetArea() const = 0;
};

Perché non semplicemente

class ThingWithArea 
{
    virtual double GetArea() const = 0;
};

ImprovedSquare è una forma ed è un ThingWithArea

Suppongo che la facciata dovrebbe fare il trucco.

Avvolgi l'interfaccia di terze parti in un'interfaccia personalizzata e il codice dell'applicazione funziona con l'interfaccia wrapper anziché con l'interfaccia di terze parti. In questo modo hai anche ben isolato le modifiche nell'interfaccia di terze parti non controllata.

Forse dovresti leggere su eredità corretta , e concludere che ImprovedShape non ha bisogno di ereditare da Shape ma invece può usare Shape per la sua funzionalità di disegno, simile alla discussione al punto 21.12 su quella FAQ su come un SortedList non deve ereditare da List anche se vuole fornire il stessa funzionalità, può semplicemente utilizzare un elenco.

In modo simile, ImprovedShape può usare una forma per fare le sue cose forma.

Forse un uso per il motivo del decoratore? [ http://en.wikipedia.org/wiki/Decorator_pattern[[1×

È possibile adottare un approccio completamente diverso, utilizzando modelli e tecniche di meta-programmazione? Se non sei obbligato a non utilizzare i modelli, questo potrebbe fornire una soluzione elegante. Solo ImprovedShape e ImprovedSquare cambiano:

template <typename ShapePolicy>
class ImprovedShape : public ShapePolicy
{
public:
    virtual double GetArea();
    ImprovedShape(void);
    virtual ~ImprovedShape(void);

protected:
    ShapePolicy shape;
    //...
};

e il ImprovedSquare diventa:

class ImprovedSquare : public ImprovedShape<Square>
{
public:
    ImprovedSquare(void);
    ~ImprovedSquare(void);

    // ...

};

Eviterai l'eredità del diamante, ottenendo sia l'eredità dalla tua Forma originale (attraverso la classe politica) sia la funzionalità aggiunta che desideri.

Un'altra interpretazione della meta-programmazione / mixin, questa volta un po 'influenzata dai tratti. Si presuppone che l'area di calcolo sia qualcosa che si desidera aggiungere in base alle proprietà esposte; potresti fare qualcosa che è rimasto con l'incapsulamento, è un obiettivo, piuttosto che una modularizzazione. Ma poi devi scrivere un GetArea per ogni sottotipo, anziché usarne uno polimorfico ove possibile. Se questo vale la pena dipende da quanto sei impegnato nell'incapsulamento e se ci sono classi base nella tua libreria di cui potresti sfruttare il comportamento comune, come RectangShape di seguito

#import <iostream>

using namespace std;

// base types
class Shape {
    public:
        Shape () {}
        virtual ~Shape () { }
        virtual void DoShapyStuff () const = 0;
};

class RectangularShape : public Shape {
    public:
        RectangularShape () { }

        virtual double GetHeight () const = 0 ;
        virtual double GetWidth  () const = 0 ;
};

class Square : public RectangularShape {
    public:
        Square () { }

        virtual void DoShapyStuff () const
        {
            cout << "I\'m a square." << endl;
        }

        virtual double GetHeight () const { return 10.0; }
        virtual double GetWidth  () const { return 10.0; }
};

class Rect : public RectangularShape {
    public:
        Rect () { }

        virtual void DoShapyStuff () const
        {
            cout << "I\'m a rectangle." << endl;
        }

        virtual double GetHeight () const { return 9.0; }
        virtual double GetWidth  () const { return 16.0; }
};

// extension has a cast to Shape rather than extending Shape
class HasArea {
    public:
        virtual double GetArea () const = 0;
        virtual Shape& AsShape () = 0;
        virtual const Shape& AsShape () const = 0;

        operator Shape& ()
        {
            return AsShape();
        }

        operator const Shape& () const
        {
            return AsShape();
        }
};

template<class S> struct AreaOf { };

// you have to have the declaration before the ShapeWithArea 
// template if you want to use polymorphic behaviour, which 
// is a bit clunky
static double GetArea (const RectangularShape& shape)
{
    return shape.GetWidth() * shape.GetHeight();
}

template <class S>
class ShapeWithArea : public S, public HasArea {
    public:
        virtual double GetArea () const
        {
            return ::GetArea(*this);
        }
        virtual Shape& AsShape ()             { return *this; }
        virtual const Shape& AsShape () const { return *this; }
};

// don't have to write two implementations of GetArea
// as we use the GetArea for the super type
typedef ShapeWithArea<Square> ImprovedSquare;
typedef ShapeWithArea<Rect> ImprovedRect;

void Demo (const HasArea& hasArea)
{
    const Shape& shape(hasArea);
    shape.DoShapyStuff();
    cout << "Area = " << hasArea.GetArea() << endl;
}

int main ()
{
    ImprovedSquare square;
    ImprovedRect   rect;

    Demo(square);
    Demo(rect);

    return 0;
}

L'approccio di Dave Hillier è quello giusto. Separare GetArea () nella propria interfaccia:

class ThingWithArea
{
public:
    virtual double GetArea() const = 0;
};

Se i progettisti di Shape avessero fatto la cosa giusta e ne avessero fatto un'interfaccia pura, e le interfacce pubbliche delle classi concrete erano abbastanza potenti, si potrebbe avere istanze di classi concrete come membri. Ecco come ottieni SquareWithArea ( ImprovedSquare è un nome scadente) essendo Shape e ThingWithArea :

class SquareWithArea : public Shape, public ThingWithArea
{
public:
    double GetPerimeter() const { return square.GetPerimeter(); }
    double GetArea() const { /* do stuff with square */ }

private:
    Square square;
};

Sfortunatamente, i progettisti Shape hanno implementato una parte in Shape e tu finirebbe per trasportarne due copie per SquareWithArea , proprio come in il diamante che hai proposto inizialmente.

Questo ti costringe praticamente nel più strettamente accoppiato, e quindi meno soluzione desiderabile:

class SquareWithArea : public Square, public ThingWithArea
{
};

Al giorno d'oggi, è considerata una cattiva forma derivare da classi concrete in C ++. È difficile trovare una spiegazione davvero valida del perché non dovresti. Di solito, le persone citare l'articolo 33 del C ++ più efficace di Meyers, che sottolinea l'impossibilità di scrivere un operator = () decente tra le altre cose. Probabilmente, quindi, dovresti mai farlo per le classi con semantica di valore. Un'altra trappola è dove la classe concreta non ha un distruttore virtuale (ecco perché dovresti mai derivare pubblicamente dai contenitori STL). Nessuno dei due si applica qui. Il poster chi ti ha inviato in modo condiscendente alla faq C ++ per conoscere l'eredità è sbagliato - l'aggiunta di GetArea () non viola la sostituibilità di Liskov. Di l'unico rischio che riesco a vedere proviene dall'override delle funzioni virtuali in classi concrete, quando l'implementatore successivamente cambia il nome e si interrompe silenziosamente il tuo codice.

In sintesi, penso che tu possa derivare da Square con la coscienza pulita. (Come consolazione, non dovrai scrivere tutte le funzioni di inoltro per l'interfaccia Shape).

Ora per il problema delle funzioni che richiedono entrambe le interfacce. Non mi piace dynamic_cast non necessari. Invece, fai in modo che la funzione accetti i riferimenti entrambe le interfacce e passano i riferimenti allo stesso oggetto per entrambi nel sito di chiamata:

void PrintPerimeterAndArea(const Shape& s, const ThingWithArea& a)
{
    cout << s.GetPerimeter() << endl;
    cout << a.GetArea() << endl;
}

// ...

SquareWithArea swa;
PrintPerimeterAndArea(swa, swa);

Tutto il PrintPerimeterAndArea () deve fare il suo lavoro è una fonte di perimetro e un fonte di area. Non è preoccupato che questi vengano implementati come funzioni membro sulla stessa istanza di oggetto. Concepibilmente, l'area potrebbe essere fornito da un motore di integrazione numerica tra esso e la Shape .

Questo ci porta all'unico caso in cui prenderei in considerazione l'idea di passare a un riferimento e ottenere l'altro per dynamic_cast - dove è importante che i due i riferimenti sono alla stessa istanza di oggetto. Ecco un esempio molto ingegnoso:

void hardcopy(const Shape& s, const ThingWithArea& a)
{
    Printer p;
    if (p.HasEnoughInk(a.GetArea()))
    {
        s.print(p);
    }
}

Anche allora, probabilmente preferirei inviare due riferimenti piuttosto che dynamic_cast . Farei affidamento su una progettazione del sistema generale sana per eliminare il possibilità di inviare bit di due istanze diverse a funzioni come questa.

GetArea () non deve essere un membro. Potrebbe essere una funzione basata su modelli, in modo da poterlo invocare per qualsiasi forma.

Qualcosa del tipo:

template <class ShapeType, class AreaFunctor> 
int GetArea(const ShapeType& shape, AreaFunctor func);

L'STL min , max possono essere pensate come un'analogia per il tuo caso. È possibile trovare un minimo e un massimo per un array / vettore di oggetti con una funzione di confronto. Come saggio, puoi derivare l'area di una determinata forma a condizione che la funzione per calcolare l'area.

Esiste una soluzione al tuo problema, poiché ho capito la domanda. Utilizza il modello di addapter . Il modello dell'adattatore viene utilizzato per aggiungere funzionalità a una classe specifica o per scambiare comportamenti particolari (ovvero metodi). Considerando lo scenario che hai dipinto:

class ShapeWithArea : public Shape
{
 protected:
  Shape* shape_;

 public:
  virtual ~ShapeWithArea();

  virtual position GetPosition() const { return shape_->GetPosition(); }
  virtual void SetPosition(position)   { shape_->SetPosition(); }
  virtual double GetPerimeter() const  { return shape_->GetPerimeter(); }

  ShapeWithArea (Shape* shape) : shape_(shape) {}

  virtual double getArea (void) const = 0;
};

L'Adattatore-Pattern ha lo scopo di adattare il comportamento o la funzionalità di una classe. Puoi usarlo per

  • cambia il comportamento di una classe, non inoltrando ma reimplementando i metodi.
  • aggiungi comportamento a una classe, aggiungendo metodi.

Come cambia il comportamento? Quando si fornisce un oggetto di tipo base a un metodo, è anche possibile fornire la classe adattata. L'oggetto si comporterà secondo le tue istruzioni, l'attore sull'oggetto si preoccuperà solo dell'interfaccia della classe base. Puoi applicare questo adattatore a qualsiasi derivato di Shape.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top