2008-10-30 3 views
4

Хорошо, у меня есть несколько сложная система на C++. Вкратце, мне нужно добавить метод к базовому классу сторонних абстракций. Третья сторона также предоставляет тонну производных классов, которые также нуждаются в новых функциях.Ищете лучший способ, чем виртуальное наследование в 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 и площади. Ну, как вы можете видеть, теперь я создал страшный diamond inheritance problem. Это можно было бы легко устранить, если библиотека сторонних разработчиков использовала virtual inheritance для своей площади, круга и т. Д. Однако заставить их делать это не разумно.

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

Спасибо!

ответ

4

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

+0

Я склоняюсь к этому методу. Я мог бы добавить метод GetShape() в ImprovedShape, который скрывает приведение. Мне жаль, что не было времени для компиляции, чтобы проверить правильность. – Imbue 2008-10-30 13:58:47

+1

Собственно, есть. Посмотрите на Boost.Type_traits - и вы должны увидеть признак производный_from <>. – 2008-10-31 07:25:05

+0

В нашем случае нам не нужна была проверка времени компиляции. Это была библиотека управления графическим интерфейсом, поэтому было бы совершенно необычно, если бы кто-нибудь написал свой собственный контроль, не наследуя от существующего. Но да, это основная проблема этого метода. – Gorpik 2008-10-31 08:30:22

4

Я полагаю, что шаблон facade должен сделать трюк.

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

+0

Это было бы безумно сложно для моего проекта. Третья библиотека также предоставляет множество функций, которые принимают формы в качестве аргументов, поэтому они также должны быть сопряжены. – Imbue 2008-10-30 14:00:33

7

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

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

Почему не просто

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

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

4

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

Аналогичным образом, ImprovedShape может использовать a Форма, чтобы сделать это Форма вещи.

1

Можно ли использовать совершенно другой подход - используя шаблоны и методы метапрограмм? Если вы не ограничены тем, что не используете шаблоны, это может обеспечить элегантное решение.Только 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); 

    // ... 

}; 

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

1

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

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

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

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

Еще один вопрос о мета-программировании/микшинге, на этот раз немного повлиянный на черты. Предполагается, что область вычисления - это то, что вы хотите добавить на основе открытых свойств; вы можете делать что-то, что хранится с инкапсуляцией, это цель, а не модуляция. Но тогда вам нужно написать 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; 
} 
0

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

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

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

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

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

1

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

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

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

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