Правильный дизайн, чтобы избежать использования dynamic_cast?

9

После некоторых исследований я не могу найти простой пример решения проблемы, с которой я часто сталкиваюсь.

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

Я бы сделал класс модели следующим образом:

class AbstractShape
{
public :
    typedef enum{
        SQUARE = 0,
        CIRCLE,
    } SHAPE_TYPE;

    AbstractShape(SHAPE_TYPE type):m_type(type){}
    virtual ~AbstractShape();

    virtual float computePerimeter() const = 0;

    SHAPE_TYPE getType() const{return m_type;}
protected :
    const SHAPE_TYPE  m_type;
};

class Square : public AbstractShape
{
public:
    Square():AbstractShape(SQUARE){}
    ~Square();

    void setWidth(float w){m_width = w;}
    float getWidth() const{return m_width;}

    float computePerimeter() const{
        return m_width*4;
    }

private :
    float m_width;
};

class Circle : public AbstractShape
{
public:
    Circle():AbstractShape(CIRCLE){}
    ~Circle();

    void setRadius(float w){m_radius = w;}
    float getRadius() const{return m_radius;}

    float computePerimeter() const{
        return 2*M_PI*m_radius;
    }

private :
    float m_radius;
};

(Представьте, что у меня есть больше классов форм: треугольники, гексагоны, каждый раз с переменными их пропрера и связанными геттерами и сеттерами. Проблемы, с которыми я столкнулся, имели 8 подклассов, но ради примера я остановился на 2)

Теперь у меня есть ShapeManager, создание и сохранение всех фигур в массиве:

class ShapeManager
{
public:
    ShapeManager();
    ~ShapeManager();

    void addShape(AbstractShape* shape){
        m_shapes.push_back(shape);
    }

    float computeShapePerimeter(int shapeIndex){
        return m_shapes[shapeIndex]->computePerimeter();
    }


private :
    std::vector<AbstractShape*> m_shapes;
};

Наконец, у меня есть представление со спинбоксами для изменения каждого параметра для каждого типа фигуры. Например, когда я выбираю квадрат на экране, виджет параметров отображает только Squareсвязанные параметры (спасибо AbstractShape::getType()) и предлагает изменить ширину квадрата. Для этого мне нужна функция, позволяющая мне изменять ширину ShapeManager, и вот как я это делаю:

void ShapeManager::changeSquareWidth(int shapeIndex, float width){
   Square* square = dynamic_cast<Square*>(m_shapes[shapeIndex]);
   assert(square);
   square->setWidth(width);
}

Есть ли лучший дизайн, позволяющий избежать использования dynamic_castи реализации пары getter / setter ShapeManagerдля каждой переменной подкласса, которую я могу иметь? Я уже пытался использовать шаблон, но не удалось .


Проблема я столкнулся на самом деле не с фигурами , но с различными Jobс для 3D - принтера (например: PrintPatternInZoneJob, TakePhotoOfZoneи т.д.) с AbstractJobкак их базового класса. Виртуальный метод есть execute()и нет getPerimeter(). Единственный раз, когда мне нужно использовать конкретное использование, это заполнить конкретную информацию, необходимую для работы :

  • PrintPatternInZone нужен список точек для печати, положение зоны, некоторые параметры печати, такие как температура

  • TakePhotoOfZone какая зона должна быть сделана на фотографии, путь, где будет сохранена фотография, размеры и т. д.

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

Единственный раз, когда мне нужно использовать конкретный тип задания, - это когда я заполняю или отображаю эту информацию (если TakePhotoOfZone Jobвыбрана буква a, будет отображаться виджет, отображающий и изменяющий параметры зоны, пути и размеров).

Затем Jobs помещаются в список Jobs, которые берут первое задание, выполняют его (вызывая AbstractJob::execute()), переходят к следующему, и так далее до конца списка. (Вот почему я использую наследование).

Для хранения различных типов параметров я использую JsonObject:

  • Преимущества: та же структура для любой работы, отсутствие динамического вещания при настройке или чтении параметров

  • проблема: не могу хранить указатели (на Patternили Zone)

Как вы думаете, есть ли лучший способ хранения данных?

Тогда как бы вы сохранили конкретный тип,Job чтобы использовать его, когда мне нужно изменить конкретные параметры этого типа? JobManagerесть только список AbstractJob*.

ElevenJune
источник
5
Похоже, ваш ShapeManager станет классом God, потому что он будет в основном содержать все методы установки для всех типов фигур.
Эмерсон Кардосо
Рассматривали ли вы дизайн «собственности мешок»? Например, changeValue(int shapeIndex, PropertyKey propkey, double numericalValue)где PropertyKeyможет быть перечисление или строка, и «Ширина» (что означает, что вызов метода установки обновит значение ширины) входит в число допустимых значений.
Руонг
Несмотря на то, что пакет свойств считается некоторыми антипаттернами OO, существуют ситуации, когда использование пакета свойств упрощает дизайн, когда любая другая альтернатива усложняет ситуацию. Тем не менее, чтобы определить, подходит ли пакет свойств для вашего случая использования, требуется больше информации (например, как код GUI взаимодействует с получателем / установщиком).
Руонг
Я рассмотрел дизайн пакета свойств (хотя я не знал его имени), но с контейнером объекта JSON. Конечно, это может сработать, но я подумал, что это не элегантный дизайн и может существовать лучший вариант. Почему это считается анти-паттерном ОО?
ElevenJune
Например, если я хочу сохранить указатель, чтобы использовать его позже, как мне это сделать?
Одиннадцать июня

Ответы:

10

Я хотел бы остановиться на «другом предложении» Эмерсона Кардозо, потому что я считаю, что это правильный подход в общем случае - хотя, конечно, вы можете найти другие решения, более подходящие для любой конкретной проблемы.

Проблема

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

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

Полезные абстракции

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

Это место, где абстракция имеет смысл. Поскольку этот модуль не касается конкретных форм, он может зависеть AbstractShapeтолько от. По той же причине, он не нуждается в getType()методе - поэтому вы должны избавиться от него.

Другие части приложения будут работать только с определенной формой, например Rectangle. Эти области не принесут пользы AbstractShapeклассу, поэтому вы не должны использовать его там. Чтобы передать только правильную форму этим деталям, вам нужно хранить конкретные формы отдельно. (Вы можете хранить их как AbstractShapeдополнительные или комбинировать их на лету).

Минимизация использования бетона

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

Как вы этого добиваетесь? Обычно, вводя больше абстракций - которые могут отражать или не отражать существующие абстракции. Например, вашему графическому интерфейсу на самом деле не нужно знать, с какой формой он имеет дело. Нужно просто знать, что на экране есть область, где пользователь может редактировать форму.

Таким образом, вы определяете реферат, ShapeEditViewдля которого у вас есть, RectangleEditViewи CircleEditViewреализации, которые содержат фактические текстовые поля для ширины / высоты или радиуса.

На первом шаге вы можете создать RectangleEditViewвсякий раз, когда создаете, Rectangleа затем помещать его в std::map<AbstractShape*, AbstractShapeView*>. Если вы предпочитаете создавать представления так, как вам нужно, вы можете сделать следующее:

std::map<AbstractShape*, std::function<AbstractShapeView*()>> viewFactories;
// ...
auto rect = new Rectangle();
// ...
auto viewFactory = [rect]() { return new RectangleEditView(rect); }
viewFactories[rect] = viewFactory;

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

Выбор правильного варианта

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

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

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

doubleYou
источник
Мне очень нравится ваш ответ, вы отлично описали проблему. Проблема, с которой я сталкиваюсь, связана не с Shapes, а с различными заданиями для 3D-принтера (например, PrintPatternInZoneJob, TakePhotoOfZone и т. Д.) С AbstractJob в качестве базового класса. Виртуальный метод - execute (), а не getPerimeter (). Единственное время, когда мне нужно использовать конкретное использование, - это заполнить конкретную информацию, необходимую для работы (список точек, положение, температура и т. Д.) Конкретным виджетом. Привязка вида к каждой работе, кажется, не то, что нужно делать в этом конкретном случае, но я не вижу, как адаптировать ваше видение к моей работе.
Одиннадцать июня
Если вы не хотите , чтобы отдельные списки, вы можете использовать viewSelector , а не viewFactory: [rect, rectView]() { rectView.bind(rect); return rectView; }. Кстати, это, конечно, должно быть сделано в модуле представления, например, в RectangleCreatedEventHandler.
дваждыВы
3
При этом постарайтесь не перегружать это. Преимущество абстракции должно все же перевесить стоимость дополнительного погружения. Иногда может быть предпочтительнее удачно подобранный состав актеров или отдельная логика.
дваждыВы
2

Один из подходов состоит в том, чтобы сделать вещи более общими , чтобы избежать приведения к конкретным типам .

В базовом классе можно реализовать базовый метод получения / установки свойств « измерения », который устанавливает значение в карте на основе определенного ключа для имени свойства. Пример ниже:

class AbstractShape
{
public :
    typedef enum{
        SQUARE = 0,
        CIRCLE,
    } SHAPE_TYPE;

    AbstractShape(SHAPE_TYPE type):m_type(type){}
    virtual ~AbstractShape();

    virtual float computePerimeter() const = 0;

    void setDimension(const std::string& name, float v){ m_dimensions[name] = v; }
    float getDimension() const{ return m_dimensions[name]; }

    SHAPE_TYPE getType() const{return m_type;}

protected :
    const SHAPE_TYPE  m_type;
    std::map<std::string, float> m_dimensions;
};

Затем в вашем классе менеджера вам нужно реализовать только одну функцию, как показано ниже:

void ShapeManager::changeShapeDimension(const int shapeIndex, const std::string& dimension, float value){
   m_shapes[shapeIndex]->setDimension(name, value);
}

Пример использования в представлении:

ShapeManager shapeManager;

shapeManager.addShape(new Circle());
shapeManager.changeShapeDimension(0, "RADIUS", 5.678f);
float circlePerimeter = shapeManager.computeShapePerimeter(0);

shapeManager.addShape(new Square());
shapeManager.changeShapeDimension(1, "WIDTH", 2.345f);
float squarePerimeter = shapeManager.computeShapePerimeter(1);

Еще одно предложение:

Поскольку ваш менеджер предоставляет доступ только к установщику и вычислению по периметру (которые также предоставляются Shape), вы можете просто создать экземпляр надлежащего View, когда создаете конкретный класс Shape. НАПРИМЕР:

  • Создание экземпляров Square и SquareEditView;
  • Передайте экземпляр Square объекту SquareEditView;
  • (необязательно) Вместо ShapeManager на главном экране вы все равно можете хранить список Shapes;
  • В SquareEditView вы сохраняете ссылку на квадрат; это исключило бы необходимость приведения для редактирования объектов.
Эмерсон Кардосо
источник
Мне нравится первое предложение, и я уже об этом думал, но оно довольно ограниченно, если вы хотите хранить разные переменные (float, указатели, массивы). Для второго предложения, если квадрат уже создан (я кликнул по нему на виде), как я узнаю, что это объект Square * ? список, хранящий фигуры, возвращает AbstractShape * .
Одиннадцать июня
@ElevenJune - да, все предложения имеют свои недостатки; во-первых, вам нужно реализовать что-то более сложное, а не простую карту, если вам нужно больше типов свойств. Второе предложение меняет способ хранения форм; Вы сохраняете базовую фигуру в списке, но в то же время вам необходимо указать ссылку конкретной фигуры на представление. Возможно, вы могли бы предоставить более подробную информацию о вашем сценарии, чтобы мы могли оценить, лучше ли эти подходы, чем просто выполнение dynamic_cast.
Эмерсон Кардосо
@ElevenJune - весь смысл в том, чтобы иметь объект просмотра, поэтому ваш графический интерфейс не должен знать, что он работает с классом типа Square. Объект представления предоставляет то, что необходимо для «просмотра» объекта (независимо от того, что вы определяете, чтобы быть), и внутренне он знает, что он использует экземпляр класса Square. Графический интерфейс взаимодействует только с экземпляром SquareView. Таким образом, вы не можете нажать на «квадратный» класс. Вы можете только нажать на класс SquareView. Изменение параметров в SquareView обновит базовый класс Square ....
Данк
... Этот подход вполне может позволить вам избавиться от вашего класса ShapeManager. Это почти наверняка упростит ваш дизайн. Я всегда говорю, что если вы называете класс менеджером, то допускаете, что это плохой дизайн, и придумываете что-то еще. Классы менеджера плохи по множеству причин, особенно из-за проблемы класса бога и того факта, что никто не знает, что класс на самом деле делает, может и не может делать, потому что менеджеры могут делать что угодно, даже косвенно связанное с тем, чем они управляют. Вы можете поспорить, что разработчики, которые следуют за вами, воспользуются преимуществом, ведущим к типичному большому мячу грязи.
Данк
1
... вы уже столкнулись с этой проблемой. С какой стати для менеджера имеет смысл быть менеджером, который меняет размеры фигуры? Зачем менеджеру рассчитывать периметр фигуры? Если вы не поняли, мне нравится "Другое предложение".
Данк