Правильний дизайн, щоб уникнути використання динамічної передачі?


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

.

Тепер у мене 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вибрано "А", буде показаний віджет, який відображає та змінює параметри зони, шляху та розмірів).

Потім Jobs вносяться до списку Jobs, який приймає перше завдання, виконує його (зателефонувавши AbstractJob::execute()), переходячи до наступного, увімкнення та продовження до кінця списку. (Ось чому я використовую спадщину).

Для зберігання різних типів параметрів я використовую JsonObject:

  • Переваги: ​​однакова структура для будь-якої роботи, відсутність динамічного_видання під час встановлення чи зчитування параметрів

  • проблема: не можна зберігати покажчики (до Patternабо Zone)

Ви вважаєте, існує кращий спосіб зберігання даних?

Тоді як би ви зберігали конкретний тип для того,Job щоб використовувати його, коли мені доведеться змінювати конкретні параметри цього типу? JobManagerмає лише список AbstractJob*.


5
Схоже, ваш ShapeManager стане класом God, тому що в основному він буде містити всі методи сеттера для всіх типів фігур.
Емерсон Кардосо

Ви розглядали дизайн "сумки власності"? Наприклад, changeValue(int shapeIndex, PropertyKey propkey, double numericalValue)де PropertyKeyможе бути перерахунок або рядок, а "Ширина" (що означає, що виклик сеттера оновить значення ширини) є одним із дозволених значень.
rwong

Навіть незважаючи на те, що майновий мішок деякими вважається захисним шаблоном OO, є ситуації, коли використання пакета властивостей спрощує дизайн, коли будь-яка інша альтернатива ускладнить справи. Хоча для визначення того, чи підходить мішок власності для вашого випадку використання, потрібно більше інформації (наприклад, як код GUI взаємодіє з геттером / сеттером).
rwong

Я розглядав дизайн сумки властивостей (хоча я не знав її назви), але з контейнером JSON. Це, звичайно, могло б спрацювати, але я подумав, що це не елегантний дизайн і що може бути кращий варіант. Чому це вважається анти-схемою ОО?
Одинадцять червня

Наприклад, якщо я хочу зберегти вказівник, щоб потім використовувати його, як це зробити?
Одинадцять червня

Відповіді:


10

Я хотів би розповісти про "іншу пропозицію" Емерсона Кардосо, тому що я вважаю, що це правильний підхід у загальному випадку - хоча, звичайно, ви можете знайти інші рішення, які краще підходять до будь-якої конкретної проблеми.

Проблема

У вашому прикладі AbstractShapeклас має getType()метод, який в основному ідентифікує конкретний тип. Це, як правило, знак того, що у вас немає хорошої абстракції. Вся суть реферату, зрештою, не турбуватися про деталі конкретного типу.

Крім того, якщо ви не знайомі з цим, вам слід ознайомитися з принципом відкритого / закритого типу. Це часто пояснюється прикладом фігур, тому ви будете почувати себе як вдома.

Корисні абстракції

Я припускаю, що ви познайомили з тим, AbstractShapeщо вважаєте корисним щось. Швидше за все, якась частина вашої програми повинна знати периметр фігур, незалежно від форми.

Це місце, де абстракція має сенс. Оскільки цей модуль не стосується конкретних форм, він може залежати AbstractShapeлише від цього. З цієї ж причини getType()метод не потребує - тому слід позбутися його.

Інші частини програми працюватимуть лише з певним видом форми, наприклад Rectangle. Ці області не отримають користі від AbstractShapeкласу, тому вам не слід його використовувати там. Для того, щоб передати цим правилам лише правильну форму, вам потрібно зберігати конкретні форми окремо. (Ви можете зберігати їх як AbstractShapeдодаткові або комбінувати їх на льоту).

Мінімізація використання бетону

Навколо цього немає ніякого способу: потрібні типи бетону в деяких місцях - принаймні при будівництві. Однак іноді найкраще обмежувати використання типів бетону лише кількома чітко визначеними областями. Ці окремі області мають єдину мету мати справу з різними типами - в той час як вся логіка застосувань утримується від них.

Як ви цього досягаєте? Зазвичай, вводячи більше абстракцій - які можуть відображати чи не відображати існуючі абстракції. Наприклад, ваш GUI НЕ дійсно потрібно знати , в якій формі він має справу. Просто потрібно знати, що на екрані є область, де користувач може редагувати фігуру.

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

На першому кроці ви можете створювати кожен RectangleEditViewраз, коли ви створюєте a, 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;

Так чи інакше, код поза цією логікою створення не повинен мати справу з конкретними формами. У рамках руйнування форми, очевидно, потрібно видалити фабрику. Звичайно, цей приклад занадто спрощений, але я сподіваюся, що ідея зрозуміла.

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

У дуже простих програмах ви можете виявити, що брудне (кастинг) рішення просто дає вам найбільше грошей на ваш долар.

Явне підтримання окремих списків для кожного конкретного типу - це, мабуть, шлях, якщо ваша програма в основному стосується конкретних форм, але має деякі частини, які є універсальними. Тут є сенс абстрагуватися лише настільки, наскільки цього вимагає загальна функціональність.

Якщо ви проходите весь шлях, зазвичай, це окупається, якщо у вас є багато логіки, яка працює на фігурах, а точний вид форми справді є деталлю вашої програми.


Мені дуже подобається ваша відповідь, ви прекрасно описали проблему. Проблема, з якою я стикаюсь, полягає не в тому, що з Shapes, а з різними роботами для 3D-принтера (наприклад: PrintPatternInZoneJob, TakePhotoOfZone тощо) з AbstractJob як базовим класом. Віртуальний метод - Execute (), а не getPerimeter (). Єдиний раз, коли мені потрібно використовувати конкретне використання, - це заповнити конкретну інформацію, яка потрібна роботі (список точок, положення, температура тощо) певним віджетом. Здається, що в цьому конкретному випадку не потрібно долучатись до кожної роботи, але я не бачу, як адаптувати ваше бачення до мого ПК.
одинадцять червня

Якщо ви не хочете зберігати окремі списки, ви можете використовувати viewSelector, а не viewFactory : [rect, rectView]() { rectView.bind(rect); return rectView; }. До речі, це, звичайно, слід робити в презентаційному модулі, наприклад, у RectangleCreateEventHandler.
подвійно Ти

3
Це, як говориться, намагайтеся не передувати це. Користь від абстракції все-таки повинна перевищувати вартість додаткового водопроводу. Іноді може бути кращим добре поставлений склад, або окрема логіка.
подвійно Ти

2

Одним із підходів було б зробити речі більш загальними , щоб уникнути лиття на конкретні типи .

Ви можете реалізувати базовий getter / setter плаваючих властивостей " розмірність " в базовому класі, який встановлює значення на карті, виходячи з конкретного ключа для імені властивості. Приклад нижче:

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), ви можете просто створити правильний вигляд при інстанціюванні певного класу Shape. EG:

  • Миттєвий розмір площі та SquareEditView;
  • Передати екземпляр Square об'єкту SquareEditView;
  • (необов’язково) Замість того, щоб мати ShapeManager, у головному режимі ви все ще можете зберігати список форм;
  • У межах SquareEditView ви зберігаєте посилання на квадрат; це усуне необхідність кастингу для редагування об'єктів.

Мені подобається перша пропозиція і я вже думав над цим, але це досить обмежує, якщо ви хочете зберігати різні змінні (float, покажчики, масиви). Що стосується другої пропозиції, якщо квадрат вже створений (я натиснув на нього на поданні), як я можу знати, що це об’єкт Square * ? список, що зберігає фігури, повертає AbstractShape * .
одинадцять червня

@ElevenJune - так, всі пропозиції мають свої недоліки; для початку вам знадобиться реалізувати щось складніше, а не просту карту, якщо вам потрібно більше типів властивостей. Друга пропозиція змінює спосіб зберігання фігур; ви зберігаєте базову форму у списку, але в той же час вам потрібно надати посилання конкретної форми на вид. Можливо, ви могли б надати більше деталей щодо свого сценарію, тому ми можемо оцінити, чи краще ці підходи, ніж просто виконання динамічної передачі.
Емерсон Кардосо

@ElevenJune - суть у тому, щоб мати об’єкт перегляду, тому ваш GUI не повинен знати, що він працює з класом типу Square. Об'єкт представлення надає необхідне для "перегляду" об'єкта (як би ви цього не визначили), і всередині він знає, що він використовує екземпляр класу Square. GUI взаємодіє лише з екземпляром SquareView. Таким чином, ви не можете натиснути клас "Квадрат". Ви можете натиснути лише на клас SquareView. Зміна параметрів на SquareView оновить базовий клас Square ....
Данк

... Цей підхід цілком може допомогти вам позбутися свого класу ShapeManager. Це майже напевно спростить ваш дизайн. Я завжди кажу, що якщо ви називаєте клас менеджером, то припускаю, що це поганий дизайн і вигадайте щось інше. Класи менеджерів погані з безлічі причин, особливо це проблема класу бога і те, що ніхто не знає, що клас насправді робить, може робити і не може, тому що менеджери можуть робити що-небудь навіть дотично пов'язане з тим, чим вони керують. Ви можете зробити ставку на те, що розробники, які слідують за вами, скористаються тим, що веде до типових грязьових балів.
Данк

1
... Ви вже зіткнулися з цією проблемою. Чому на землі має сенс менеджер бути таким, який змінює розміри форми? Чому менеджер обчислює периметр форми? Якщо ви цього не зрозуміли, мені подобається "Ще одна пропозиція".
Данк
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.