вторник, 4 января 2011 г.

Вдогонку к предыдущему посту. Мы всегда, неосознанно, ищем во всем связи, даже если никакой связи на самом деле нет. Проявляется это по разному, например, многие хоккеисты во время плей-офф не бреются, а тренеры не меняют рубашки :) Программисты в этом деле - впереди планеты всей, ведь у нас есть специальный инструмент для внесения в код подобных заблуждений - наследование! Если между сущностями есть связь - то их можно объединить в иерархию наследования. Пожалуй, самый яркий пример подобного заблуждения встречается в литературе, в качестве примера объектно ориентированного программирования - иерархия фигур в векторном графическом редакторе, знаменитый Shape и его наследники - Line, Rectangle и прочие. (Справедливости ради стоит отметить, что автор этих строк, когда-то давно, написал простой векторный граф. редактор используя именно такой подход :D) Итак, у нас есть базовый абстрактный класс - Shape, у которого есть набор виртуальных методов для: вывода себя на экран/графический контекст; получения bounding box-а объекта; трансформации объекта... Далее, программист должен реализовать классы - наследники Shape, которые специализируют все операции для определенной фигуры. Плюсы этого подхода - кажущаяся простота кода, вроде все в одном месте, а также повторное использование кода, к примеру, класс Circle может быть наследником класса Ellipse. Минусы несколько менее очевидны: сложность внесения изменений - на каждую новую фигуру нам потребуется реализовать новый класс - наследник Shape и реализовать соответствующие вирт. методы, а если мы хотим добавить новую операцию, для которой нужен еще один вирт. метод, то нам придется реализовать его для каждого существующего класса - наследника Shape; не очень хорошая производительность - у нас целых два уровня косвенности - указатель на объект и виртуальный метод этого объекта. И самый главный недостаток этого подхода - нам не нужно знать о том, что та или иная фигура является окружностью или линией, это не имеет значения, так зачем же нам поддерживать иерархию наследования? Иногда самый простой и очевидный способ решения проблемы - самый правильный. Однако, в данном случае, наиболее простой и эффективный метод решения достаточно не очевиден и даже контр-интуитивен. Представим, что у нас есть два массива - массив точек и массив метаданных. Массив точек содержит координаты всех точек чертежа: начало и конец каждой линии; начало, конец и контрольные точки каждого спалйна, точки заливки... Массив метаданных должен содержать информацию о том, к чему относится та или иная точка, а также прочую информацию об объекте: начало линии, конец линии, начало сплайна, конец сплайна, изменить цвет линии, изменить цвет заливки и тд. В этом случае, для того, что-бы отобразить все на экране, нам потребуются два индекса/указателя на текущую позицию в каждом из массивов, далее, в цикле мы будем выбирать поочередно метаданные и соответствующие им точки. К примеру, если в текущей позиции массива метаданных находится команда LINE, то, начиная с текущей позиции массива точек, должны быть выбраны две точки - координаты начала и конца линии и, используя текущий цвет фигуры, нарисована линия; если мы встретили в метаданных команду FLOOD_FILL, то из массива точек следует выбрать одну точку и выполнить заливку текущим цветом фона из этой точки. Минусы этого подхода - ну очень непонятно для поклонников GoF, Грэди Буча и Скотта Мейерса. Плюсы: очень легко изменять код - набор изначальных примитивов очень ограничен и не меняется, операции реализуются для примитивов, не для фигур, один раз; эффективность - нет никаких указателей, смарт-поинтеров, виртуальных методов, многие операции могут быть реализованы очень просто, например, для поворота всего чертежа, достаточно умножить каждую точку массива на соответствующую матрицу поворота. Конечно, это не означает, что наследование это плохо, или, что следует отказаться от ООП, просто следует помнить о том, что то, что у вас в голове выстраивается в иерархию, на самом деле иерархией может и не быть. Фигуры чертежа состоят из примитивов - линий, сплайнов. То, что из этих линий и сплайнов можно создать более сложные фигуры, которые могут быть объединены в иерархию - может быть совсем не важно для решения задачи.

6 комментариев:

NightmareZ комментирует...

А, если теперь нужно удалить круг из чертежа? Или повернуть квадрат, а не весь чертёж? Или пользователь решил передвинуть на другое место круг, квадрат и треугольник?

З.Ы. Пост - бред и ересь.

Lazin комментирует...

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

Анонимный комментирует...

http://insidecpp.ru/art/8/

Анонимный комментирует...

Удачи Вам в Новом Году!
И много-много хороших статей и обзоров.

legolegs комментирует...

на каждую новую фигуру нам потребуется реализовать новый класс - наследник Shape и реализовать соответствующие вирт. методы, а если мы хотим добавить новую операцию, для которой нужен еще один вирт. метод, то нам придется реализовать его для каждого существующего класса - наследника Shape
Не совсем понятно, что вам тут не нравится. Да, если мы добавим метод посчитать_площадь() - то его придётся реализовывать отдельно для каждого примитива. Это логично и компилятор нам поможет, чтоб мы ничего не забыли.

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

К примеру, если в текущей позиции массива метаданных находится команда LINE, то, начиная с текущей позиции массива точек, должны быть выбраны две точки - координаты начала и конца линии и, используя текущий цвет фигуры, нарисована линия
Вот о чём я говорил. Вы изобрели OpenGL. Он сложный. Сложнее, чем
  рисунок.добавить(новый кружочек(8);
  длявсех(фигурка в рисунок)
    фигурка.рисуйся(принтер);

legolegs комментирует...

на каждую новую фигуру нам потребуется реализовать новый класс - наследник Shape и реализовать соответствующие вирт. методы, а если мы хотим добавить новую операцию, для которой нужен еще один вирт. метод, то нам придется реализовать его для каждого существующего класса - наследника Shape
Не совсем понятно, что вам тут не нравится. Да, если мы добавим метод посчитать_площадь() - то его придётся реализовывать отдельно для каждого примитива. Это логично и компилятор нам поможет, чтоб мы ничего не забыли.

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

К примеру, если в текущей позиции массива метаданных находится команда LINE, то, начиная с текущей позиции массива точек, должны быть выбраны две точки - координаты начала и конца линии и, используя текущий цвет фигуры, нарисована линия
Вот о чём я говорил. Вы изобрели OpenGL. Он сложный. Сложнее, чем
  рисунок.добавить(новый кружочек(8);
  длявсех(фигурка в рисунок)
    фигурка.рисуйся(принтер);