В поисках лучшего способа, чем виртуальное наследование в C ++ - PullRequest
4 голосов
/ 30 октября 2008

ОК, у меня несколько сложная система на C ++. Короче говоря, мне нужно добавить метод в сторонний абстрактный базовый класс. Третья сторона также предоставляет тонну производных классов, которые также нуждаются в новой функциональности.

Я использую библиотеку, которая предоставляет стандартный интерфейс Shape, а также некоторые распространенные формы.

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

Теперь вот моя проблема. Я хочу, чтобы класс Shape также включал функцию GetArea (). Так что, похоже, я должен просто сделать:

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

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

А потом я делаю и создаю ImprovedSquare, который наследуется от ImprovedShape и Square. Ну, как вы можете видеть, я создал страшную проблему наследования алмазов . Это легко исправить, если сторонняя библиотека использует виртуальное наследование для своих квадратов, окружностей и т. Д. Однако заставить их сделать это не является разумным вариантом.

Итак, что вы делаете, когда вам нужно добавить немного функциональности в интерфейс, определенный в библиотеке? Есть хороший ответ?

Спасибо!

Ответы [ 10 ]

7 голосов
/ 30 октября 2008

Почему этот класс должен быть производным от формы?

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

Почему бы просто не иметь

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

ImprovedSquare - это форма и является ThingWithArea

4 голосов
/ 30 октября 2008

Возможно, вам следует прочитать правильное наследование и сделать вывод, что ImprovedShape не нужно наследовать от Shape, но вместо этого можно использовать Shape для его функций рисования, аналогично обсуждению в пункте 21.12 этого FAQ на что SortedList не должен наследовать от List, даже если он хочет предоставить ту же функциональность, он может просто использовать List.

Аналогичным образом ImprovedShape может использовать Shape для создания объектов Shape.

4 голосов
/ 30 октября 2008

У нас была очень похожая проблема в проекте, и мы решили ее, просто НЕ извлекая ImprovedShape из Shape. Если вам нужна функциональность Shape в ImprovedShape, вы можете dynamic_cast, зная, что ваше приведение будет работать всегда. А все остальное как в твоем примере.

4 голосов
/ 30 октября 2008

Полагаю, что шаблон фасад должен помочь.

Оберните сторонний интерфейс в собственный интерфейс, и код вашего приложения будет работать с интерфейсом-оболочкой, а не со сторонним интерфейсом. Таким образом, вы также хорошо изолировали изменения в неконтролируемом стороннем интерфейсе.

2 голосов
/ 30 октября 2008

Возможно использование для шаблона декоратора? [http://en.wikipedia.org/wiki/Decorator_pattern][1]

1 голос
/ 29 сентября 2010

GetArea () не обязательно должен быть участником. Это может быть шаблонная функция, так что вы можете вызывать ее для любой фигуры.

Что-то вроде:

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

Функции STL min , max могут рассматриваться как аналогия для вашего случая. Вы можете найти минимальное и максимальное значения для массива / вектора объектов с помощью функции сравнения. Точно так же вы можете получить площадь любой заданной фигуры при условии, что эта функция рассчитывает площадь.

1 голос
/ 02 ноября 2008

Еще один взгляд на метапрограммирование / миксин, на этот раз под влиянием черт. Предполагается, что вычисление площади - это то, что вы хотите добавить на основе открытых свойств; Вы можете сделать что-то, что связано с инкапсуляцией, это цель, а не модульность. Но тогда вы должны написать GetArea для каждого подтипа, а не использовать полиморфный, где это возможно. То, стоит ли это, зависит от того, насколько вы привержены инкапсуляции, и есть ли в вашей библиотеке базовые классы, которые вы могли бы использовать, например, RectangularShape ниже

#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;
}
1 голос
/ 02 ноября 2008

Подход Дейва Хиллиера является правильным. Разделите GetArea() на собственный интерфейс:

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

Если дизайнеры Shape правильно сделали и сделали его чистым интерфейсом, и публичные интерфейсы конкретных классов были достаточно мощными, вы могли бы есть экземпляры конкретных классов в качестве членов. Вот как вы получаете SquareWithArea (ImprovedSquare - плохое имя), будучи Shape и ThingWithArea:

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

private:
    Square square;
};

К сожалению, Shape разработчики вложили некоторую реализацию в Shape, и вы в конечном итоге несет две копии этого в SquareWithArea, как в алмаз, который вы изначально предложили.

Это в значительной степени вынуждает вас в наиболее тесно связанных, и, следовательно, наименее желательно, решение:

class SquareWithArea : public Square, public ThingWithArea
{
};

В наши дни считается плохой формой наследования конкретных классов в C ++. Трудно найти действительно хорошее объяснение, почему вы не должны. Обычно люди процитировать более эффективный пункт 33 Мейерса C ++, который указывает на невозможность написания приличного operator=() среди прочего. Вероятно, тогда вы должны никогда не делайте это для классов с семантикой значения. Еще одна ловушка, где конкретный класс не имеет виртуального деструктора (вот почему вы должны никогда публично не наследовать от контейнеров STL). Ни то, ни другое здесь не применимо. Плакат кто снисходительно послал вас в C ++ часто задаваемые вопросы, чтобы узнать о наследовании неправильно - добавление GetArea() не нарушает подставляемость Лискова. Около единственный риск, который я вижу, связан с переопределением виртуальных функций в конкретные классы, когда разработчик позже меняет имя и молча ломает ваш код.

Подводя итог, я думаю, что вы можете сделать вывод из Квадрата с чистой совестью. (В качестве утешения вам не придется писать все функции пересылки для интерфейс Shape).

Теперь о проблеме функций, которым нужны оба интерфейса. Мне не нравится ненужные dynamic_cast с. Вместо этого заставьте функцию принимать ссылки на оба интерфейса и передают ссылки на один и тот же объект для обоих на сайте вызова:

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

// ...

SquareWithArea swa;
PrintPerimeterAndArea(swa, swa);

Все, что нужно PrintPerimeterAndArea() для выполнения своей работы - это источник периметра и Источник области. Это не его забота о том, чтобы это произошло как функции-члены на одном экземпляре объекта. Возможно, область могла быть снабжен каким-то механизмом численного интегрирования между ним и Shape.

Это приводит нас к единственному случаю, когда я рассмотрел бы передачу в одной ссылке и получить другой dynamic_cast - где важно, чтобы два ссылки на один и тот же экземпляр объекта. Вот очень надуманный пример:

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

Даже тогда я бы предпочел послать две ссылки, а не dynamic_cast. Я бы положился на здравый общий дизайн системы, чтобы устранить возможность использования битов двух разных экземпляров для таких функций.

1 голос
/ 30 октября 2008

Можно ли использовать совершенно другой подход - использовать шаблоны и методы метапрограммирования? Если вы не ограничены в использовании шаблонов, это может стать элегантным решением. Только ImprovedShape и ImprovedSquare изменяются:

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

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

и ImprovedSquare становится:

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

    // ...

};

Вы избежите наследования бриллиантов, получая как наследство от вашей первоначальной формы (через класс политики), так и дополнительные функции, которые вам нужны.

0 голосов
/ 19 ноября 2008

Существует решение вашей проблемы, как я понял вопрос. Используйте шаблон addapter . Шаблон адаптера используется для добавления функциональности к определенному классу или для обмена определенным поведением (то есть методами). Учитывая сценарий, который вы нарисовали:

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;
};

Шаблон адаптера предназначен для адаптации поведения или функциональности класса. Вы можете использовать его для

  • изменить поведение класса, не перенаправляя, а переопределяя методы.
  • добавить поведение в класс, добавив методы.

Как это меняет поведение? Когда вы предоставляете объект типа base для метода, вы также можете предоставить адаптированный класс. Объект будет вести себя так, как вы указали, субъект объекта будет заботиться только об интерфейсе базового класса. Вы можете применить этот адаптер к любому производному Shape.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...