LINUX.ORG.RU

Элегантное решение

 ,


0

1

Всем привет! Это продолжение предыдущей темы, в которой я хочу найти элегантное решение.

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

class Shape{
  virtual void Center() = 0;
}

class Line: public Shape{
  void Center() {...};
  // нет процедуры вычисления площади
}

class Circle: public Shape{
  void Center() {...};
  double Square() {...}; // вычисляется площадь
}

void FindCenter(vector<Shape*> shape) {
  for (vector<Shape*> it = shape.begin(); it!=shape.end(); ++it)
    (*it)->Center();
}

void FindSquare(vector<Shape*> shape) {
  double s=0;
  
  // Нужно вычислить лишь для третьей фигуры ее площадь
  s += shape[2].Square(); // Здесь происходит ошибка.
  std::cout << s << std:endl;
}

int main() {
  vector<Shape*> shape;
  Line line1, line2;
  Circle circle;

  shape.push_back(&line1);
  shape.push_back(&line2);
  shape.push_back(&circle);

  FindCenter(vector<Shape*> shape); // печатается координаты центра фигуры
  FindSquare(vector<Shape*> shape); // найти общую площадь
  return 0;
}
★★★★★

Последнее исправление: Zodd (всего исправлений: 1)

vector<Shape*> shape
s += shape[2].Square(); // Здесь происходит ошибка.

Правильно, что происходит.

andreyu ★★★★★
()

vector<Shape*> shape
shape[2].Square();

у тебя же вектор указателей, соотв.

shape[2]->Square();

но для этого, класс Shape должен иметь этот метод Square(), а все наследники определять его:

class Shape {
  virtual double Square() = 0;
  virtual void Center() = 0;
}


если кто-то из наследников не может вычислить площадь - пусть вернет 0.

dib2 ★★★★★
()

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

Если тебя устраивает, что у линий будет нулевая площадь - ну так и сделай так. Если хочется, то можно вообще реализацию возвращающую ноль в базовый интерфейс вынести. Если же хочется именно костылей - ну так делай dynamic_cast и проверяй результат.

Ну и от функций с названием файнд_что-то я бы точно не ожидал void в качестве возвращаемого значения. Логичнее было бы возвращать значение, а печатать его уже снаружи.

DarkEld3r ★★★★★
()
Последнее исправление: DarkEld3r (всего исправлений: 1)
Ответ на: комментарий от dib2

если кто-то из наследников не может вычислить площадь - пусть вернет 0.

должен бахнуть исключение, на самом деле. exception это сообщение не об ошибке, а о «необычном/нетипичном» случае.

в родителе считается что у всех фигур есть вычислимый центр, но вдруг потомок неспособен его найти - это же ЖОПА. А жопа - это exception

MKuznetsov ★★★★★
()
Ответ на: комментарий от MKuznetsov

можно и exception, но слишком радикально как на меня. забодает потом обкладывать ловушками по циклам.

dib2 ★★★★★
()
Ответ на: комментарий от DarkEld3r

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

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

Ну и от функций с названием файнд_что-то я бы точно не ожидал void в качестве возвращаемого значения. Логичнее было бы возвращать значение, а печатать его уже снаружи.

Так это лишь пример, а не реальная функция.

Zodd ★★★★★
() автор топика
Ответ на: комментарий от dib2

если кто-то из наследников не может вычислить площадь - пусть вернет 0.

Похоже так и придется делать, другого я не придумал.

Zodd ★★★★★
() автор топика
Ответ на: комментарий от dib2

если кто-то из наследников не может вычислить площадь - пусть вернет 0.

Чисто для самообразования: Анафига его чисто виртуальным в этом случае делать? почему тут не годится сделать виртуальный, который вернет 0?

anonymous
()
Ответ на: комментарий от Zodd

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

Нет, не то. Ещё раз - если мы хотим со всеми элементами работать одинаково, используя наследование, то сложить все в один вектор нормально. В таком случае, возвращать нулевую площадь имеет смысл. Хотя без подробностей мне это кажется кривым решением.

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

Но опять же, я понятия не имею, что у тебя за задача.

DarkEld3r ★★★★★
()

Ничего не понимаю... И это программисты... Стандарт дал им <algorithm> и итераторы — пользуйся! Не хочу, хочу велосипедить говно...

anonymous
()

В принципе для подобного случая можно попробовать применить паттерн Визитер.

grondek
()
Ответ на: комментарий от DarkEld3r

У этих классов почти все схоже, кроме некоторых функций. Поэтому таскать их в одном векторе удобно. А задача у меня связана с конечными элементами.

Zodd ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Анонимус совсем плохой пошел. Даже не читает уже.

Zodd ★★★★★
() автор топика

Подскажите как обойти эту ошибку

Ты всё сделал не так. Во-первых Line и Circle не должны быть наследниками Shape. Они должны быть объектами Shape. Сам Shape должен хранить всю информацию о фигуре, а именно набор узловых точек и кривизну. Далее, делаешь фабрику, которая фигачит объекты Shape по заданным данным, например если нужна линия, вызываешь MakeLine() который возвращает объект типа Shape представляющий собой линию, MakeCircle(), который конструирует круг из двух кривых и т.д. Соответственно делаешь методы Shape, которые определяют, является ли объект линией, кругом и т.д. Отдельно мутишь калькулятор центра, площади и прочей шелухи.

no-such-file ★★★★★
()
Ответ на: комментарий от no-such-file

Вроде понял что ты хотел сказать. Возник вопрос: как связаны объекты и калькулятор?

Zodd ★★★★★
() автор топика
Ответ на: комментарий от Zodd

Можно подробнее?

Shape - это фигура, набор точек, соединенных линиями с некоторой кривизной, будь то прямая, круг или неправильный семиугольник с гранями в виде кривых безье. Для упрощения работы нужны фабрика которая будет делать специфичные фигуры, например круг по центру и радиусу, а не по двум точкам на окружности + кривизна.

Возник вопрос: как связаны объекты и калькулятор?

Калькулятор, это отдельный класс объектов, которые получая объект shape могут производить над ним нужные вычисления. Отдельный класс нужен для того, чтобы не хардкодить алгоритмы внутри Shape. Фабрика объектов-калькуляторов получив объект shape должна сделать соответствующий конкретной фигуре объект-калькулятор. Кроме того понятие «центр» для сложных фигур можно трактовать по-разному и для этого тоже могут использоваться разные объекты-калькуляторы.

Т.е. в Shape не должно быть никаких Center() или Square(), там должны быть только методы для доступа к точкам/кривым - один класс, одна задача. В данном случае хранение информации о форме отдельный класс, вычисления отдельный класс, а ещё отдельно можно сделать например класс для трансформации объекта Shape - переносы, повороты и т.п.

no-such-file ★★★★★
()
Последнее исправление: no-such-file (всего исправлений: 2)
Ответ на: комментарий от anonymous

И фабрику фабрик

Разумеется. Покрыть всё это сверху абстрактной фабрикой, которая выдаёт конкретные фабрики в зависимости от конфигурации записаной в XML было бы здорово. Но это задача следующей итерации - на данном этапе ТСу нужно разобраться просто с фабриками.

no-such-file ★★★★★
()
Ответ на: комментарий от no-such-file

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

Zodd ★★★★★
() автор топика
Ответ на: комментарий от Zodd

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

передаю ему

Не ему, а его объекту, а так да. В итоге должно выглядеть примерно так:

Shape *s = makeShape(Shape::TRIANGLE, 100, 100, 50, 50, 0, 0);
Transform *t = makeTransform(Transform::ROTATE, 90);
s = t->apply(s);

no-such-file ★★★★★
()
Ответ на: комментарий от Zodd

Ты предлагаешь первый подход?

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

В случае абстрактной фабрики код будет примерно такой:

Fabric *maker = makeFabric("config.xml"); 
Shape *s = maker->makeShape(Shape::TRIANGLE, 100, 100, 50, 50, 0, 0);
Transform *t = maker->makeTransform(Transform::ROTATE, 90);
s = t->apply(s);
Идея в том, что у тебя может быть 2 разные реализации Shape, например хранящие прямоугольные или полярные координаты точек. Но тогда Calc и Transrom тоже должны иметь 2 реализации, учитывающие эту особенность. Поэтому, чтобы не хардкодить инстанциирование первой или второй реализации, ты просто делаешь фабрику объектом и имеешь 2 реализации фабрики - для первого и для второго набора Shape/Calc/Transform. Такой подход удобен ещё и при тестировании, т.к. ты можешь в зависимости от необходимости инстанциировать тестовые варианты объектов вместо рабочих.

Кроме того, я показал фабрику как свободную функцию только для простоты. А вообще фабрику удобно получать из реестра приложения - синглтона хранящего общие для приложения объекты.

Shape *s = App()->maker->makeShape(Shape::TRIANGLE, 100, 100, 50, 50, 0, 0);
Transform *t = App()->maker->makeTransform(Transform::ROTATE, 90);
s = t->apply(s);

В этом случае синглтон инстанциирует фабрику при первом обращении.

В общем, тут много что можно сделать, тебе сначала стоит подумать, а надо ли тебе так всё усложнять?

no-such-file ★★★★★
()
Ответ на: комментарий от no-such-file

Спасибо за разъяснения, ведь я по образованию не программист.
Вот сделал пока такую реализацию вычислений. Что здесь надо править?

Осталось понять как к нему приделать класс Transform.

#include <iostream>
#include <vector>
#include <cassert>
#include <cmath>

using namespace std;

enum Shape_ID {Line_ID, Rect_ID, Circle_ID};

class Shape {
  public:
    double x1, y1, x2, y2;
    virtual double getSquare() = 0;
    virtual ~Shape() {}

    static Shape* createShape(Shape_ID id, double xBeg, double yBeg, double xEnd, double yEnd);
};

class Line: public Shape {
  public:
    double getSquare() {
      return 0;
    }
};

class Rect: public Shape {
  public:
    double getSquare() {
      return abs((x2-x1)*(y2-y1));
    }
};

class Circle: public Shape {
  public:
    double getSquare() {
      return 3.14*abs((x2-x1)*(y2-y1));
    }
};

Shape* Shape::createShape(Shape_ID id, double xBeg, double yBeg, double xEnd, double yEnd) {
    Shape * p;

    switch (id) {
        case Line_ID:
            p = new Line();
            break;
        case Circle_ID:
            p = new Circle();
            break;
        case Rect_ID:
            p = new Rect();
            break;
        default:
            assert(false);
    }
    p->x1 = xBeg;
    p->x2 = xEnd;
    p->y1 = yBeg;
    p->y2 = yEnd;
    return p;
}

int main() {
    vector<Shape*> v;
    v.push_back( Shape::createShape(Line_ID,0,0,1,1));
    v.push_back( Shape::createShape(Rect_ID,1,1,2,2));
    v.push_back( Shape::createShape(Circle_ID,2,1,3,3));

    for(size_t i=0; i<v.size(); i++)
        cout << v[i]->getSquare() << endl;
    // ...

    return 0;
}
Zodd ★★★★★
() автор топика
Ответ на: комментарий от no-such-file

В целом я разобрался, но так и не понял как прикрутить Transform к классу Shape. Не подскажешь как они должны быть связаны? Т.е. через какой механизм их связи использовать? Заранее спасибо.

Zodd ★★★★★
() автор топика
Ответ на: комментарий от Zodd

как они должны быть связаны?

Они не должны быть связаны. Объект Transform берет объект Shape и меняет координаты соответственно своему назначению либо делает новый объект Shape с новыми координатами.

Shape *s = makeShape(Shape::CIRCLE, 100, 100, 200, 200);
Transform *t = makeTransform(Transform::MOVE, 10, 10);
s = t->apply(s); // Взять s, вычислить новые координаты, сделать новый Shape или изменить переданный.

Если t->apply просто меняет координаты переданного объекта, то проблем вообще нет. Если же t->apply должен сделать новый Shape, то проблема в том, как он это сделает. Самое очевидное решение использовать копирующий конструктор Shape, но его для этого нужно определить.

no-such-file ★★★★★
()
Ответ на: комментарий от no-such-file

Спасибо, понял. Дальше уже справлюсь.

Zodd ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.