После некоторых исследований я не могу найти простой пример решения проблемы, с которой я часто сталкиваюсь.
Допустим, я хочу создать небольшое приложение, в котором я могу создавать Square
s, Circle
s и другие фигуры, отображать их на экране, изменять их свойства после их выбора, а затем вычислять все их периметры.
Я бы сделал класс модели следующим образом:
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, будет отображаться виджет, отображающий и изменяющий параметры зоны, пути и размеров).
Затем Job
s помещаются в список Job
s, которые берут первое задание, выполняют его (вызывая AbstractJob::execute()
), переходят к следующему, и так далее до конца списка. (Вот почему я использую наследование).
Для хранения различных типов параметров я использую JsonObject
:
Преимущества: та же структура для любой работы, отсутствие динамического вещания при настройке или чтении параметров
проблема: не могу хранить указатели (на
Pattern
илиZone
)
Как вы думаете, есть ли лучший способ хранения данных?
Тогда как бы вы сохранили конкретный тип,Job
чтобы использовать его, когда мне нужно изменить конкретные параметры этого типа? JobManager
есть только список AbstractJob*
.
источник
changeValue(int shapeIndex, PropertyKey propkey, double numericalValue)
гдеPropertyKey
может быть перечисление или строка, и «Ширина» (что означает, что вызов метода установки обновит значение ширины) входит в число допустимых значений.Ответы:
Я хотел бы остановиться на «другом предложении» Эмерсона Кардозо, потому что я считаю, что это правильный подход в общем случае - хотя, конечно, вы можете найти другие решения, более подходящие для любой конкретной проблемы.
Проблема
В вашем примере у
AbstractShape
класса естьgetType()
метод, который в основном идентифицирует конкретный тип. Как правило, это признак того, что у вас нет хорошей абстракции. В конце концов, весь смысл абстрагирования заключается не в том, чтобы заботиться о деталях конкретного типа.Кроме того, в случае, если вы не знакомы с ним, вы должны прочитать об открытом / закрытом принципе. Это часто объясняется примером формы, поэтому вы будете чувствовать себя как дома.
Полезные абстракции
Я предполагаю, что вы ввели,
AbstractShape
потому что вы нашли это полезным для чего-то. Скорее всего, какая-то часть вашего приложения должна знать периметр фигур, независимо от того, что это за форма.Это место, где абстракция имеет смысл. Поскольку этот модуль не касается конкретных форм, он может зависеть
AbstractShape
только от. По той же причине, он не нуждается вgetType()
методе - поэтому вы должны избавиться от него.Другие части приложения будут работать только с определенной формой, например
Rectangle
. Эти области не принесут пользыAbstractShape
классу, поэтому вы не должны использовать его там. Чтобы передать только правильную форму этим деталям, вам нужно хранить конкретные формы отдельно. (Вы можете хранить их какAbstractShape
дополнительные или комбинировать их на лету).Минимизация использования бетона
Обойти это невозможно: в некоторых местах вам нужны конкретные типы - по крайней мере, во время строительства. Тем не менее, иногда лучше ограничить использование типов бетона несколькими четко определенными областями. Эти отдельные области имеют единственную цель иметь дело с различными типами - в то время как вся логика приложения не учитывается.
Как вы этого добиваетесь? Обычно, вводя больше абстракций - которые могут отражать или не отражать существующие абстракции. Например, вашему графическому интерфейсу на самом деле не нужно знать, с какой формой он имеет дело. Нужно просто знать, что на экране есть область, где пользователь может редактировать форму.
Таким образом, вы определяете реферат,
ShapeEditView
для которого у вас есть,RectangleEditView
иCircleEditView
реализации, которые содержат фактические текстовые поля для ширины / высоты или радиуса.На первом шаге вы можете создать
RectangleEditView
всякий раз, когда создаете,Rectangle
а затем помещать его вstd::map<AbstractShape*, AbstractShapeView*>
. Если вы предпочитаете создавать представления так, как вам нужно, вы можете сделать следующее:В любом случае, код вне этой логики создания не должен иметь дело с конкретными формами. Как часть разрушения фигуры, вам нужно убрать фабрику, очевидно. Конечно, этот пример слишком упрощен, но я надеюсь, что идея ясна.
Выбор правильного варианта
В очень простых приложениях вы можете обнаружить, что грязное (литье) решение дает вам максимальную отдачу.
Явное ведение отдельных списков для каждого конкретного типа - это, вероятно, путь, если ваше приложение в основном работает с конкретными фигурами, но содержит некоторые части, которые являются универсальными. Здесь имеет смысл абстрагироваться только тогда, когда этого требует общая функциональность.
Движение до конца обычно окупается, если у вас много логики, которая работает с фигурами, и точный вид фигуры действительно является деталью вашего приложения.
источник
[rect, rectView]() { rectView.bind(rect); return rectView; }
. Кстати, это, конечно, должно быть сделано в модуле представления, например, в RectangleCreatedEventHandler.Один из подходов состоит в том, чтобы сделать вещи более общими , чтобы избежать приведения к конкретным типам .
В базовом классе можно реализовать базовый метод получения / установки свойств « измерения », который устанавливает значение в карте на основе определенного ключа для имени свойства. Пример ниже:
Затем в вашем классе менеджера вам нужно реализовать только одну функцию, как показано ниже:
Пример использования в представлении:
Еще одно предложение:
Поскольку ваш менеджер предоставляет доступ только к установщику и вычислению по периметру (которые также предоставляются Shape), вы можете просто создать экземпляр надлежащего View, когда создаете конкретный класс Shape. НАПРИМЕР:
источник