Параллельное наследование между интерфейсными классами и классами реализации в C ++ - PullRequest
5 голосов
/ 12 ноября 2010

Я пытаюсь использовать абстрактный базовый класс C ++ аналогично интерфейсу Java.Предположим, что у нас есть следующие классы интерфейса только с чисто виртуальными функциями:

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

, и я пытаюсь реализовать Square и Rectangle следующим образом:

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

Где RectangleImpl наследует оба SquareImpl и Rectangle для повторного использования, скажем, SquareImpl::area().Однако, когда я пытаюсь скомпилировать, возникают две проблемы: во-первых, все методы в SquareImpl не наследуются должным образом, и мне приходится вручную переопределять RectangleImpl::area() и RectangleImpl::setLength().Во-вторых, это все еще приводит к проблеме алмаза в том, что Shape является неоднозначным основанием RectangleImpl.

Я мог бы скомпилировать код, если бы я фактически наследовал Square от Shape, но я не думаю, чтопроизводительность будет масштабироваться с добавлением дополнительных производных интерфейсов.Также странно, что RectangleImpl все еще не наследует SquareImpl::setLength(), хотя SquareImpl::area() наследуется хорошо.(игнорируйте практичность здесь)

Другое решение может состоять в том, чтобы сделать интерфейсы независимыми друг от друга, то есть сделать Square не унаследованным от Shape.Но из-за этого я потеряю доступ к методам в Shape, если я определю функции, которые принимают указатель Square*.Это также сделает static_cast невозможным между Shape и Square.

Так что мой вопрос в том, существует ли какой-либо другой шаблон проектирования в C ++ для решения такого параллельного наследования между интерфейсными классами и классами реализации, не требуявиртуальное наследование?

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

Ответы [ 6 ]

3 голосов
/ 12 ноября 2010

Здесь у вас есть случай Алмазной проблемы , которая может возникнуть на любом языке OO, допускающем множественное наследование. Это, кстати, одна из причин, почему разработчики Java решили не иметь множественное наследование, и пришли к идее интерфейса.

C ++ решает проблему с алмазом следующим образом: Виртуальное наследование .

И, как указал codymanix, квадрат и прямоугольник - это заведомо плохой пример для объектно-ориентированного проектирования, поскольку для ОО квадрат не является прямоугольником .

Пара больше очков. Во-первых, термин для того, что вы здесь делаете, - множественное наследование , а не "параллельное наследование". Во-вторых, в данном конкретном случае не имеет смысла иметь class Square и class SquareImpl. Если вы считаете, что у вас могут быть разные реализации Square, у вас должен быть только один базовый класс, который обеспечивает реализацию по умолчанию и виртуальные функции, которые при необходимости могут быть переопределены производным классом. Другими словами, вы должны свернуть Square и SquareImpl в один класс с виртуальными функциями.

Вы, конечно, можете использовать абстрактный класс C ++, такой как интерфейс Java, но в большинстве случаев для этого нет причин. Интерфейсы были добавлены в Java именно как способ обойти отсутствие множественного наследования. В C ++ вы можете просто пойти дальше и использовать множественное наследование, хотя вы всегда должны делать это очень разумно.

2 голосов
/ 12 ноября 2010

Вы далеко не первый, кто столкнулся с этой проблемой. См. A Квадрат не является прямоугольником , чтобы привести один пример.

0 голосов
/ 13 ноября 2010

После переосмысления на ночь и обращения к решению, которое Шон предоставил на В поисках лучшего способа, чем виртуальное наследование в C ++ , я предложил следующее решение.

Здесь я переопределяюпроблема быть более абстрактной, чтобы избежать путаницы в формах.У нас есть интерфейс Ball, который может катиться, интерфейс FooBall, который содержит методы, специфичные для Foo, и интерфейс FooBarBall, который также является FooBall и содержит методы, специфичные для Foo и Bar.Так же, как и в оригинальной задаче, у нас есть реализация FooBall, и мы хотим вывести ее, чтобы охватить также специфические методы Bar.но наследование как интерфейса, так и реализации приведет к алмазному наследованию.

Чтобы решить эту проблему, вместо непосредственного помещения специфических методов Foo и Bar в производные интерфейсы Ball, я поместил один метод в производное FooBall интерфейс, который преобразует объект в Foo объект с помощью метода toFoo().Таким образом, реализации могут смешиваться в независимом интерфейсе Foo и Bar, не вводя алмазное наследование.

Тем не менее, не все коды могут быть удалены, чтобы свободно выводить все столбцы из Foos.Нам все еще нужно написать независимые реализации Ball, FooBall и FooBarBall, которые не наследуются друг от друга.Но мы можем использовать составной шаблон, чтобы обернуть реальные Foo и Bar объекты, которые реализованы по-разному.Таким образом, мы все еще можем исключить много кода, если у нас много реализаций Foo и 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;

}
0 голосов
/ 12 ноября 2010

Ваша проблема в Rectangle-> Square-> Shape ничего не знает о SquareImpl, поэтому он не может использовать эти функции для удовлетворения своих требований к абстрактным функциям.

Самый простой способ - не смешивать ваш интерфейс и наследование реализации, когда они так тесно связаны. Заставьте RectangleImpl наследовать от интерфейсов Square и Rectangle. Если SquareImpl и RectangleImpl реплицируют слишком много кода друг друга, используйте класс, который выполняет всю эту работу и имеет функцию-член в каждой реализации.

0 голосов
/ 12 ноября 2010

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

Не совсем уверен в проблеме производительности, о которой вы упомянули - мне это не кажется проблемой (в том смысле, что это не дополнительные издержки, кроме вызова любой v-функции).Я не понимаю, почему вы не можете получить доступ к setLength из квадрата - трудно понять, почему вы можете испытывать это, и у вас нет источника для реализаций.

0 голосов
/ 12 ноября 2010

Квадрат не является прямоугольником, а прямоугольник не является квадратом.Единственное, что у них общего, - это то, что они являются формами.Итак:

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

Их функции инициализации различны, Square::setSide(double) и Rectangle::setLengthAndWidth(double, double).Вам не нужны * классы Impl.Делай свои вещи в Square и Rectangle.

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