Гради Буч - Объектно-ориентированный анализ и проектирование с примерами приложений на С++ (1158635), страница 66
Текст из файла (страница 66)
Для монолитныхабстракций подобные операции можно назвать "глубокими", а длякомпозитных абстракций - "поверхностными", в том смысле, что прикопировании происходит передача ссылки на часть общей структуры.Семейства классовТретий основной принцип проектирования библиотеки заключается впостроении семейств классов, связанных отношением наследования.
Длякаждого типа структур мы создадим несколько различных классов,объединенных единым интерфейсом (как в случае с абстрактным классомQueue), но с разными конкретными подклассами, имеющими несколькоразличные представления и поэтому отличающимися Своим устройством ихарактеристиками "время/память".
Таким образом мы обеспечимбиблиотечное требование полноты. Разработчик сможет выбрать тотконкретный класс, который в большей степени подходит для решения егозадачи. В то же время этот класс обладает тем же интерфейсом, что иостальные классы семейства. Сознательное четкое разделение абстрактногобазового класса и его конкретных подклассов позволяет пользователюсистемы выбрать, скажем, на первом этапе проектирования один из классов вкачестве рабочего, а затем, в процессе доводки приложения, заменить его надругой, чем-то отличающийся класс того же семейства, затратив на этоминимум времени и усилий (единственное, что ему потребуется, - это зановооттранслировать свою программу). При этом разработчик будет уверен внормальном функционировании программы, так как все классы,принадлежащие одному семейству, обладают идентичным внешниминтерфейсом и схожим поведением.
Смысл в такой организации классовсостоит еще и в возможности копирования, присваивания и сравненияобъектов одного семейства даже в том случае, если их представлениясовершенно разнятся.Можно сказать, что базовый абстрактный класс как бы содержит всебе все важные черты абстракции. Другое важное применение абстрактныхбазовых классов - это кэширование общего состояния, которое дороговычислять заново. Так можно перевести вычисление O(n) в операцию порядкаO(1) - простое считывание данных.
При этом, естественно, требуетсяобеспечить соответствующий механизм взаимодействия между абстрактнымбазовым классом и его подклассами, чтобы гарантировать актуальностькэшируемого значения.Элементы семейства классов представляют собой различные формыабстракции. Опыт показывает, что существуют две основные формыабстракций, которыми следует пользоваться разработчику при созданиисерьезных приложений. Во-первых, это форма конкретного представленияабстракции в оперативной памяти машины. Существует два варианта такогопредставления: выделение памяти для структуры из стека или выделениеоперативной памяти из кучи. Им соответствуют две формы абстракций:ограниченная и неограниченная:• ОграниченнаяСтруктура хранится в стеке и, таким образом,имеет статический размер (известный в момент создания объекта).• НеограниченнаяСтруктура хранится в куче и ее размеры могутдинамически изменяться.Так как ограниченная и неограниченная формы абстракции имеютобщие интерфейс и поведение, их обе можно представить в виде прямыхподклассов абстрактного базового класса для каждой структуры.
Мы обсудимэти и другие особенности организации данных в следующих разделах.Второй вариант связан с синхронизацией. Как было отмечено в главе2, множество полезных приложений обходятся одним процессом. Ихназывают последовательными системами, потому что они используют одинпоток управления.
Для других приложений (особенно это касается системреального времени) требуется обеспечить синхронизацию несколькиходновременно выполняемых потоков. Такие системы называютсяпараллельными, и в них каким-то образом должно обеспечиваться взаимноеисключение процессов, конкурирующих за один и тот же ресурс.
Ясно, чтонельзя дать возможность управлять одним и тем же объектом одновременнонескольким потокам, это в конце концов приведет к нарушению егосостояния. Рассмотрим, например, поведение двух агентов, которыеодновременно пытаются добавить элемент одному и тому же объекту классаQueue. Первый агент, начавший добавление элемента, может быть прерванраньше, чем окончит данную операцию, и оставит объект второму агенту внезавершенном состоянии.Рис.
9-3. Семейства классовКак отмечалось в главе 3, в данном случае при проектированиисуществуют всего три возможных альтернативы, каждая из которых требуетобеспечения различного уровня взаимодействия между агентами,оперирующими с общими объектами:• последовательный• защищенный• синхронизированный.Мы рассмотрим каждый из этих вариантов более подробно вследующем разделе. Обеспечение взаимодействия между абстрактнымбазовым классом, формами его представления и формами синхронизациипорождает для каждой структуры семейство классов, подобное тому, котороеприведено на рис.
9-3. Теперь можно понять, почему мы в свое время решилиорганизовать библиотеку именно в виде семейств классов, а не в виде единогодерева. Это было сделано из-за того, что такая архитектура:• Отражает общность различных форм.• Позволяет осуществлять более простой доступ к элементамбиблиотеки.• Позволяет избежать бесконечных метафизических споров о "чистомобъектно-ориентированном подходе".• Упрощает интеграцию системы с другими библиотеками.МикроорганизацияВ целях обеспечения простоты работы с системой выберем одинобщий стиль оформления структур и механизмов библиотеки:template<...>class Name : public Superclass {public:// конструкторы // виртуальный деструктор// операторы// модификаторы// селекторыprotected:// данные// служебные функцииprivate:// Друзья};Описание абстрактного базового класса Queue начинается следующимобразом:template<class Item> class Queue {Сигнатура шаблона template служит для задания аргументовпараметризованного класса.
Отметим, что в C++ шаблоны сознательновведены таким образом, чтобы передать достаточную гибкость (иответственность) в руки разработчика, инстанцирующего шаблон в своемприложении.Далее определим обычный список конструкторов и деструкторов:Queue();Queue(const Queue<Item>&);virtual -Queue() ;Отметим, что мы описали деструктор виртуальным, чтобы обеспечитьполиморфное поведение при уничтожении объектов класса. Далее объявимвсе операторы:virtual Queue<Item>& operator=(const Queue<ltem>&) ;virtual int operator==(const Queue<Item>&) const;int operator!=(const Queue<Item>&) const;Мы определили оператор присваивания (operator==) и операторсравнения (operator==) как виртуальные для того, чтобы обеспечитьбезопасность типов. Переопределение этих операторов входит в обязанностиподклассов.
В них будут использоваться функции, аргументом которыхявляется объект собственного специализированного класса. В этом смыслеподклассы имеют то преимущество, что они знают представление своихэкземпляров и могут обеспечить очень эффективную реализацию. Когдаконкретный подкласс очереди неизвестен (например, если мы передаем объектпо ссылке на его базовый класс), вызывается оператор базового класса,использующий может быть менее эффективные, но более универсальныеалгоритмы. Эта идиома имеет побочный эффект: возможность работы одной итой же функции с очередями, имеющими различную внутреннюю реализацию,без нарушения типизации.Если мы хотим ограничить доступ к копированию, присваиванию илисравнению некоторых объектов, нам надо объявить эти операторызащищенными или закрытыми.Определим теперь модификаторы, позволяющие менять состояниеобъекта:virtual void clear() = 0;virtual void append(const Item&) = 0;virtual void pop() =0;virtual void remove (unsigned int at) = 0;Данные операции объявлены как чисто виртуальные, а это значит, чтоих описание является обязанностью подклассов.
Наличие чисто виртуальныхфункций делает класс Queue абстрактным.Спецификатор const указывает (компилятор может это проверить) наиспользование функций-селекторов, то есть функций, предназначенныхисключительно для получения информации о состоянии объекта, но не дляизменения состояния:virtualvirtualvirtualvirtualunsigned int length() const = 0;int isEmpty() const = 0;const Item& front() const =0;int location(const Item&) const = 0;Эти операции тоже определены как чисто виртуальные, потому чтокласс Queue не обладает достаточной информацией для их полного описания.Защищенная часть каждого класса начинается с описания техэлементов, которые формируют основу его структуры и должны бытьдоступны подклассам.22 Абстрактный класс Queue, в. отличие от своихподклассов (см.
ниже), подобных элементов не имеет.Продолжит защищенную часть базового класса определениеслужебных функций, которые будут полиморфно реализованы в конкретныхподклассах. Класс Queue содержит довольно типичный список такихфункций:virtualvirtualvirtualvirtualvirtualvirtualvoid purge () =0;void add(const Item&) = 0;unsigned int cardinality() const = 0;const Item& itemAt (unsigned int) const =0;void lock();void unlock();Причины, по которым мы ввели именно эти функции, будутрассмотрены в следующем разделе.И, наконец, определим закрытую часть, обычно содержащуюобъявления о классах-друзьях и те элементы, которые мы хотим сделатьнедоступными даже для подклассов.