Ориентация объекта: как выбрать из ряда реализаций

я приличный процедурный программист, но я новичок в объектной ориентации (я был обучен как инженер на старом добром Паскале и C). Что я нахожу особенно сложным, так это выбор одного из нескольких способов достижения того же самого. Это особенно верно для C++, потому что его сила позволяет вам делать почти все, что вам нравится, даже ужасные вещи (я думаю, что здесь уместна поговорка власти/ответственности).

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

в качестве упражнения я разрабатываю простой симулятор, где "геометрическое представление "может быть двух типов:" круг "или"многоугольник". Другие части симулятора затем должны будут принять эти представления и потенциально иметь дело с ними иначе. Я придумал, по крайней мере, четыре разных способа сделать это. Каковы достоинства/недостатки/недостатки каждой из них?

A: Перегрузка Функции

объявить Circle и Polygon как несвязанные классы, а затем перегрузите каждый внешний метод, требующий геометрического представления.

B: Кастинг

объявления enum GeometricRepresentationType {Circle, Polygon}. Объявить абстрактное GeometricRepresentation класс и наследовать Circle и Polygon от него. GeometricRepresentation виртуальные GetType() метод, который реализуется с помощью Circle и Polygon. Затем методы используют GetType() и оператор switch для приведения GeometricRepresentation к соответствующему типу.

C: не уверен в подходящем имени

объявить тип перечисления и абстрактный класс, как в B. В этом классе также создаются функции Circle* ToCircle() {return NULL;} и Polygon* ToPolygon() {return NULL;}. Затем каждый производный класс перегружает соответствующую функцию, возвращая this. Это просто новое изобретение динамического литья?

D: Соберите Их Вместе

реализуйте их как один класс, имеющий член перечисления, указывающий, какой тип является объектом. Класс содержит члены, которые могут хранить оба представления. Тогда внешние методы не должны вызывать глупые функции (например,GetRadius() на полигон или GetOrder() по кругу).

4 ответов


вот несколько правил дизайна (большого пальца), которые я преподаю своим студентам OO:

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

2) каждый раз, когда вы пишете оператор if об объекте (или его текущем состоянии/режиме/любом другом), вы можете (вероятно, лучше) сделать вызов виртуальной функции для выполнения некоторой (более абстрактной) операции, где исходный оператор then - or else-sub-является телом виртуальной функции производного объекта.

например, вместо этого:

if (obj->type() == CIRCLE) {
    // do something circle-ish
    double circum = M_PI * 2 * obj->getRadius();
    cout << circum;
}
else if (obj->type() == POLY) {
    // do something polygon-ish
    double perim = 0;
    for (int i=0; i<obj->segments(); i++)
        perm += obj->getSegLength(i);
    cout << perim;
}

этого:

cout << obj->getPerimeter();

...

double Circle::getPerimeter() {
    return M_PI * 2 * obj->getRadius();
}

double Poly::getPerimeter() {
    double perim = 0;
    for (int i=0; i<segments(); i++)
        perm += getSegLength(i);
    return perim;
}

в приведенном выше случае довольно очевидно, что такое" более абстрактная " идея, периметр. Так будет не всегда. Иногда у него даже не будет хорошего имени, что является одной из причин, почему его трудно "увидеть". Но вы можете преобразовать любой оператор if в вызов виртуальной функции, где " if" часть заменяется виртуальностью функции.

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


скорее всего, у вас будут общие методы между Polygon и Circle. Я бы объединил их обоих под интерфейсом с именем Shape, например (писать на java, потому что это свежее в моем уме синтаксически. Но это то, что я бы использовал, если бы написал пример c++. Прошло некоторое время с тех пор, как я написал c++):

public interface Shape {
   public double getArea();

   public double getCentroid();

   public double getPerimiter(); 
}

и Polygon и Circle реализовать этот интерфейс:

public class Circle implements Shape {
   // Implement the methods
}

public class Polygon implements Shape {
   // Implement the methods
}

что вы получаете:

  1. вы всегда можно лечить Shape как обобщенный объект с определенными свойствами. Вы сможете добавить different Shape реализации в будущем без изменения кода, который что-то делает с Shape (если у вас нет чего-то конкретного для нового Shape)

  2. если у вас есть методы, которые точно такие же, вы можете заменить интерфейс абстрактным классом и реализовать их (в интерфейсе C++ это просто абстрактный класс ни с чем реализовано)

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


пройдите учебник по основам C++ и прочитайте что-то вроде "языка программирования C++" Страуструпа, чтобы узнать, как использовать язык идиоматически.

не поверьте, люди говорят вам, что вам придется изучать ООП независимо от языка. Грязный секрет заключается в том, что то, что каждый язык понимает как ООП, ни в коем случае даже отдаленно не похоже в некоторых случаях, поэтому наличие твердой базы, например Java, на самом деле не является большой помощью для C++; это идет так далеко, что язык go просто нет классов вообще. Кроме того, C++-это явно многопарадигмальный язык, включающий процедурное, объектно-ориентированное и общее программирование в одном пакете. Тебе нужно научиться эффективно сочетать это. Он был разработан для максимальной производительности, что означает, что некоторые из более низких битных вещей показывают, оставляя многие связанные с производительностью решения в руках программиста, где другие языки просто не дают опций. C++ имеет очень обширный библиотека общих алгоритмов, обучение их использованию является обязательной частью учебной программы.

Не волнуйтесь по поводу "эффективности", используйте виртуальные функции-члены везде, если нет веской причины не делать этого. Получите хорошее представление о ссылках и const. Получение права на дизайн объекта-это очень тяжелый, не ожидайте первая (или пятая) попытка стать последней.


во-первых, немного фона на ООП и как отличаются C++ и другие языки, такие как Java.


люди склонны использовать объектно-ориентированное программирование для несколько различных целей:

  1. обобщенное программирование: написание кода, который является общим; т. е. который работает на любом объекте или данных, который предоставляет указанный интерфейс, без необходимости заботиться о реализации подробности.

  2. модульность и инкапсуляция: предотвращение слишком тесной связи различных частей кода друг с другом (так называемая "модульность"), скрывая от пользователей не относящиеся к делу детали реализации.
    Это другой способ думать о разделение.

  3. статический полиморфизм: настройка реализации" по умолчанию " некоторого поведения для определенного класса объекты при сохранении модульного кода, где набор возможных настроек уже известно когда вы пишете свою программу.
    (Примечание: Если вам не нужно было сохранять модульный код, то выбор поведения будет таким же простым, как if или switch, но тогда исходный код должен будет учитывать все возможности.)

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

в Java, те же инструменты (наследование и переопределение) используются для решения в основном все из этих проблем.
Плюс в том, что есть только один способ решить все проблемы, так легче учиться.
Нижняя сторона иногда-но-не-всегда-незначительное наказание за эффективность: решение, которое решает проблему № 4, дороже, чем решение, которое нужно только решить № 3.

теперь введите C++.

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

  1. обобщенное программирование: C++ templates сделаны для этого. Они похожи на дженерики Java, но на самом деле дженерики Java часто требуются наследование будет полезно, тогда как шаблоны C++ не имеют ничего общего с наследованием в целом.

  2. модульность и инкапсуляция: классы C++ есть public и private модификаторы доступа, как и в Java. В этом отношении два языка очень похожи.

  3. статический полиморфизм: Java не имеет способа решить это особенности проблема, а вместо этого заставляет вас использовать решение для #4, заплатив штраф, который вам не обязательно платить. В C++, с другой стороны, использует комбинацию template classes и наследование называется CRTP чтобы решить эту проблему. этот тип наследования очень отличается от того, для #4.

  4. динамический полиморфизм: C++ и Java допускают наследование и переопределение функций и похожи в этом отношении.


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

вероятно, лучший способ (хотя, возможно, самый сложный способ) - использовать #3 для этой задачи.

если нужно, вы можете реализовать #4 поверх него для классов, которые в нем нуждаются, не затрагивая другие классы.

вы объявляете класс под названием Shape и определите базовую функциональность:

class Graphics;  // Assume already declared

template<class Derived = void>
class Shape;   // Declare the shape class

template<>
class Shape<>  // Specialize Shape<void> as base functionality
{
    Color _color;
public:
    // Data and functionality for all shapes goes here
    // if it does NOT depend on the particular shape
    Color color() const { return this->_color; }
    void color(Color value) { this->_color = value; }
};

затем вы определяете функциональные возможности:

template<class Derived>
class Shape : public Shape<>   // Inherit base functionality
{
public:
    // You're not required to actually declare these,
    // but do it for the sake of documentation.
    // The subclasses are expected to define these.
    size_t vertices() const;
    Point vertex(size_t vertex_index) const;

    void draw_center(Graphics &g) const { g.draw_pixel(shape.center()); }

    void draw_outline()
    {
        Derived &me = static_cast<Derived &>(*this);  // My subclass type
        Point p1 = me.vertex(0);
        for (size_t i = 1; i < me.vertices(); ++i)
        {
            Point p2 = me.vertex(1);
            g.draw_line(p1, p2);
            p1 = p2;
        }
    }

    Point center() const  // Uses the methods above from the subclass
    {
        Derived &me = static_cast<Derived &>(*this);  // My subclass type
        Point center = Point();
        for (size_t i = 0; i < me.vertices(); ++i)
        { center += (center * i + me.vertex(i)) / (i + 1); }
        return center;
    }
};

как только вы это сделаете, вы можете определить новые формы:

template<>
class Square : public Shape<Square>
{
    Point _top_left, _bottom_right;
public:
    size_t vertices() const { return 4; }

    Point vertex(size_t vertex_index) const
    {
        switch (vertex_index)
        {
        case 0: return this->_top_left;
        case 1: return Point(this->_bottom_right.x, this->_top_left.y);
        case 2: return this->_bottom_right;
        case 3: return Point(this->_top_left.x, this->_bottom_right.y);
        default: throw std::out_of_range("invalid vertex");
        }
    }

    // No need to define center() -- it is already available!
};

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

надеюсь, что это помогает.