понедельник, 29 ноября 2010 г.

Проектирование реализации

Цель этапа проектирование классов – найти подходящие классы и определить требования к ним.

Цель этапа проектирование реализации – изобрести реализацию класса, удовлетворяющую требованиям к нему.

В предложенном мною подходе логика проектирования может быть выражена формулой:

Функциональная модель => Группы функций => Кандидаты в классы => Реализация

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

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

Далее, он выявляет классы, даёт им названия и определяет требования к ним. Как правило, классами становятся либо группы однородных функций, сформированные на предыдущем этапе, либо блоки данных, с которыми эти функции работают.

И, наконец, разработчик подбирает реализацию для выявленных классов.

При проектировании реализации важно сделать две вещи:

  1. определиться с данными, которые класс будет хранить;
  2. подобрать оптимальные структуры для хранения этих данных.

Рассмотрим это на нашем сквозном примере.

В предыдущей статье был выявлен класс Контур. Его обязанность – представление практически любой (прямолинейной и криволинейной) формы фигуры.

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



В качестве кривой обычно используют кривую Безье 3-го порядка. У неё есть ряд преимуществ, которые и предопределили широкое использование кривой в компьютерной графике.



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

Во-вторых, афинные преобразования кривой Безье (перенос, масштабирование, вращение) могут быть осуществлены путём применения этих трансформаций к опорным точкам.

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

В-четвёртых, с помощью кривых Безье второго и третьего порядка  можно аппроксимировать дугу эллипса. Варианты аппроксимации расписаны здесь.

Более детально о кривых Безье можно почитать здесь и здесь.

Определимся с реализацией класса Контур. Логика ООП подсказывает представить его в виде набора сегментов, каждый из которых может быть либо отрезком прямой линии, либо кривой.

class Contour
{
      std::vector<Segment *> m_Segments;
};

class Segment;
class Line : public Segment;
class Curve : public Segment;

Не смотря на "логичность", у такого решения есть несколько недостатков:

1. Излишний расход памяти и дублирование данных.

Отрезок прямой линии обычно задаётся двумя точками. Если фигура состоит из нескольких отрезков, то промежуточные точки (== точки стыка) будут продублированы.

Отрезок прямой линии может быть задан:

1)      либо двумя точками,
2)      либо точкой и вектором.

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

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

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

1)      сначала мы, поддавшись стремлению к абстракции, создаём себе проблему (дублирование точек),
2)      а затем – успешно её решаем.

По-моему, это выглядит странно.

2. Усложнение кода.

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



Если представить контур в виде последовательности сегментов, то код доступа ко второй опорной точке i-ой кривой будет выглядеть излишне сложно:

Segment * pSegment = GetSegment(i);
Curve * pCurve = dynamic_cast<Curve *>(pSegment);

if (pCurve)
   pCurve->GetPoint(1);

Поскольку операции редактирования

a)      больше связаны с точками, а не с сегментами,
b)      требуют информации о точках соседних кривых

- то логичнее представить контур не в виде набора сегментов, а в виде набора точек и их атрибутов.

class Contour
{
      std::vector<Point>          m_Points;
      std::vector<unsigned char>  m_Flags;
};

Атрибуты точки характеризуют её роль:

Атрибут
Роль
Normal
Начальная или конечная точка кривой или линии
Smooth
Гладкий переход между соседними кривыми
Symmetrical
Гладкий и симметричный переход между соседними кривыми
Control
Управляющая точка кривой

При таком представлении любая точка контура может быть получена за один шаг:

Point p = GetPoint(i);

3 комментария:

  1. Очень легко читается, спасибо. Мне бы хотелось узнать ваше мнение о распределении ролей в этих этапах (определение функциональности, проектирование классов...). То есть _кто_ предполагаемый исполнитель в каждом из этапов, какими знаниями он должен обладать? На ком, в теории, лежит ответственность за принятие решений?
    В вашем изложении это один человек, и, надо признать, весьма эрудированный и профессиональный человек. Средний программист (не разработчик) реализует Figure, я уверен. Затем, возможно, прочитает ваш блог и займется рефакторингом, но тем не менее.

    + за Безье спасибо отдельное.

    ОтветитьУдалить
  2. Рад, что статья оказалась для Вас полезной. :)

    У нас в компании работает такая схема:

    1) На средних проектах декомпозицию задачи на подзадачи (http://askofen.blogspot.com/2010/10/blog-post_31.html) выполняет технический руководитель проекта. Он же берет на себя решение наиболее сложной подзадачи.

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

    ОтветитьУдалить
  3. Спасибо за цикл. Предыдущая статья про проектирование классов пока лучшая в цикле. В ней вы заострили внимание на том, что классы суть группы функций. И что от них следует выводить классы.

    В этой статье все слишком "на пальцах" получилось. Не сказано самое главное- данные нужны для хранения состояний объектов. Состояние в свою очередь бывают внешние, которыми "мыслит" пользователь. И внутренние которые определяются реализациями алгоритмов и должны скрываться.
    Как-то так.

    ОтветитьУдалить