пятница, 19 ноября 2010 г.

Проектирование классов

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

Классический объектно-ориентированный подход даёт проектировщику чёткое руководство, как это делать.

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

Затем следует установить взаимосвязи между ключевыми абстракциями. Если взаимосвязь можно выразить отношением is-a, то такие абстракции следует связать при помощи  наследования. Если взаимосвязь подходит под отношение has-a, то такие абстракции следует связать при помощи агрегации.

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

Получается стройная и логичная картина:

class Figure;
class Rectangle : public Figure;
class Ellipse   : public Figure;
class Triangle  : public Figure;

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


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

Графический редактор должен позволять преобразовывать:

  • эллипс – в последовательность кривых;
  • прямоугольник – в многоугольник;
  • отрезок – в кривую;
  • последовательность кривых – в кривую;
  • кривую – в последовательность кривых:

а также – находить:

  • общий контур для группы фигур;
  • пересечение двух фигур;
  • разницу двух фигур;
  • отрицание фигуры.

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

Такое решение кажется разумным лишь в учебном примере. Его использование в серьёзном приложении порождает ряд критичных проблем:

  1. Правильно ли представлять прямоугольник и квадрат отдельными классами?
  2. Что из чего правильнее выводить – прямоугольник из квадрата или квадрат из прямоугольника?
  3. Как представить пересечение прямоугольника с эллипсом?
  4. Следует ли для пересечения создать отдельный класс?
  5. Что будет, если полученное пересечение мы пересечём ещё один раз?
  6. Какой класс будем использовать для представления результата?
  7. Как добавлять в программу новые фигуры?
  8. Если нам потребуется 115 фигур, то означает ли это, что мы должны написать 115 классов?

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

Но абстракции абстракциям – рознь. Полезность объекта определяется не его названием, а его функциональным наполнением. Важно смотреть не на то, как объект называется, а на то, какие он выполняет функции. Класс должен браться не из предметной области, а выводиться из функциональной модели на основе общности функций.

Можно дать такое определение:

Класс – это группа похожих функций.

Или ещё короче:

1 класс == 1 обязанность

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

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

Функциональная модель --> Группа похожих функций --> Структура данных

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

Контура можно:

  1. создавать;
  2. объединять;
  3. пересекать;
  4. вычитать;
  5. отрицать;
  6. и т.д.

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