Цель этапа проектирование классов – найти подходящие классы и определить требования к ним.
Цель этапа проектирование реализации – изобрести реализацию класса, удовлетворяющую требованиям к нему.
В предложенном мною подходе логика проектирования может быть выражена формулой:
Функциональная модель => Группы функций => Кандидаты в классы => Реализация
Прежде всего, разработчик анализирует работу пользователя с проектируемой системой и строит функциональную модель системы.
Затем, он расширяет функциональную модель и помещает найденные функции в группы по принципам сходства или противоположности.
Далее, он выявляет классы, даёт им названия и определяет требования к ним. Как правило, классами становятся либо группы однородных функций, сформированные на предыдущем этапе, либо блоки данных, с которыми эти функции работают.
И, наконец, разработчик подбирает реализацию для выявленных классов.
При проектировании реализации важно сделать две вещи:
- определиться с данными, которые класс будет хранить;
- подобрать оптимальные структуры для хранения этих данных.
Рассмотрим это на нашем сквозном примере.
В предыдущей статье был выявлен класс Контур. Его обязанность – представление практически любой (прямолинейной и криволинейной) формы фигуры.
Следуя рекомендациям, прежде всего определимся с данными, с помощью которых будем представлять контур. Поскольку контур может содержать как прямолинейные, так и криволинейные участки, то, очевидно, его можно представить с помощью последовательности отрезков прямых и кривых линий.
В качестве кривой обычно используют кривую Безье 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;
};
Атрибуты точки характеризуют её роль:
Атрибут
|
Роль
|
Начальная или конечная точка кривой или линии
| |
Smooth
|
Гладкий переход между соседними кривыми
|
Symmetrical
|
Гладкий и симметричный переход между соседними кривыми
|
Control
|
Управляющая точка кривой
|
При таком представлении любая точка контура может быть получена за один шаг:
Point p = GetPoint(i);
Очень легко читается, спасибо. Мне бы хотелось узнать ваше мнение о распределении ролей в этих этапах (определение функциональности, проектирование классов...). То есть _кто_ предполагаемый исполнитель в каждом из этапов, какими знаниями он должен обладать? На ком, в теории, лежит ответственность за принятие решений?
ОтветитьУдалитьВ вашем изложении это один человек, и, надо признать, весьма эрудированный и профессиональный человек. Средний программист (не разработчик) реализует Figure, я уверен. Затем, возможно, прочитает ваш блог и займется рефакторингом, но тем не менее.
+ за Безье спасибо отдельное.
Рад, что статья оказалась для Вас полезной. :)
ОтветитьУдалитьУ нас в компании работает такая схема:
1) На средних проектах декомпозицию задачи на подзадачи (http://askofen.blogspot.com/2010/10/blog-post_31.html) выполняет технический руководитель проекта. Он же берет на себя решение наиболее сложной подзадачи.
2) Остальные подзадачи делегируются опытным инженерам, которые вырабатывают решения и согласуют их с техническим руководителем проекта.
Спасибо за цикл. Предыдущая статья про проектирование классов пока лучшая в цикле. В ней вы заострили внимание на том, что классы суть группы функций. И что от них следует выводить классы.
ОтветитьУдалитьВ этой статье все слишком "на пальцах" получилось. Не сказано самое главное- данные нужны для хранения состояний объектов. Состояние в свою очередь бывают внешние, которыми "мыслит" пользователь. И внутренние которые определяются реализациями алгоритмов и должны скрываться.
Как-то так.