Гради Буч - Объектно-ориентированный анализ и проектирование с примерами приложений на С++ (1158635), страница 65
Текст из файла (страница 65)
В обоихслучаях реализации нижнего уровня может быть продемонстрированакаждому пользователю, но только при его желании.Рассмотрим описание абстракций нашей библиотеки с двух точекзрения:пользователя, который только объявляет объекты уже существующихклассов, и клиента, который конструирует собственные подклассы на базебиблиотечных. При проектировании с расчетом на первого пользователяжелательно как можно сильнее ограничить доступ к реализациям абстракцийи сконцентрироваться на их ответственностях; проектирование с учетомзапросов второго пользователя предполагает открытость некоторыхвнутренних деталей реализации, однако, не настолько, чтобы сталовозможным нарушить фундаментальную семантику абстракции. Такимобразом, приходится отметить некоторую противоречивость основныхтребований к системе.Одной из главных проблем при работе с большой библиотекойявляются трудности в понимании того, какие, собственно, механизмы онавключает в себя.
Перечисленные выше модели представляют собой как быдушу архитектуры библиотеки:чем больше разработчик знает об этих механизмах, тем легче емубудет использовать существующие в библиотеке компоненты, а не сочинять снуля собственные. На практике получается так, что пользователь сначалазнакомится с содержанием и работой наиболее простых классов, и толькозатем, проверив надежность их работы, постепенно начинает использовать всеболее сложные классы.
В процессе разработки, по мере того как начинаютвырисовываться новые, присущие предметной области пользователя,абстракции, они тоже могут добавляться в библиотеку. Развитие объектноориентированной библиотеки - это длительный процесс, проходящий черезряд промежуточных этапов.Именно так мы будем строить нашу библиотеку: сначала определимтот архитектурный минимум, который реализует все пять выделенных намимеханизмов, и затем начнем постепенно наращивать на этом остове все новыеи новые функции.9.2.
ПроектированиеТактические вопросыВ соответствии с законом разработки программ Коггинса "прагматизмвсегда должен быть предпочтительней элегантности, ведь Природу все равноничем не удивить". Следствие: проектирование никогда не будет полностьюнезависимым от языка реализации проекта. Особенности языка неизбежноналожат отпечаток на те или иные архитектурные решения, и ихигнорирование может привести к тому, что нам придется работать вдальнейшем с абстракциями, не в полной мере учитывающими преимуществаи недостатки конкретного языка реализации.Как было отмечено в главе 3, объектно-ориентированные языкипредоставляют три основных механизма упорядочения большего числаклассов: наследование, агрегацию и параметризацию.
Наследование являетсянаиболее популярным свойством объектно-ориентированной технологии,однако далеко не единственным принципом структурирования. Как мыувидим, сочетание параметризации с наследованием и агрегацией помогаетсоздать достаточно мощную и в то же время компактную архитектуру.Рассмотрим усеченное описание предметно-зависимого класса очередив C++:class NetworkEvent... // сетевое событиеclass EventQueue {// очередь событийpublic:EventQueue();virtual ~EventQueue();virtual void clear(); // очиститьvirtual void add(const NetworkEvent&); // добавитьvirtual void pop(); // продвинутьvirtual const NetworkEvent& front() const; // первыйэлемент...};Перед нами абстракция, олицетворяющая очередь событий: структура,в которую мы можем добавлять новые элементы в конец очереди и удалятьэлементы из начала очереди.
C++ позволяет скрыть внутренние деталиреализации класса очереди за его внешним интерфейсом (операциями clear ,add, pop и front ).Нам могут потребоваться также некоторые другие варианты очереди,например, приоритетная очередь, где события выстраиваются в соответствии сих срочностью. Разумно воспользоваться результатами уже проделаннойработы и организовать новый класс на базе ранее определенного:class PriorityEventQueue : public EventQueue {public:PriorityEventQueue();virtual ~PriorityEventQueue();virtual void add(const NetworkEvent&);…};Виртуальность функций (например функции add) поощряетпереопределение операций в подклассах.Комбинация наследования с параметризованными классами позволяетсоздавать еще более общие абстракции.
Семантика класса очереди не зависитот того, что в ней: волки или овцы. Используя классы-шаблоны, можнопереопределить наш базовый класс следующим образом:template<class Item>class Queue {public:Queue ();virtual ~Queue() ;virtual void clear();virtual void add(const Item&);virtual void pop();virtual const Item& front() const;…};Это наиболее распространенный способ использованияпараметризованных классов: взять существующий конкретный класс,выделить в нем то, что не зависит от элементов, с которыми он оперирует, исделать эти элементы аргументами шаблона.Наследование и параметризация очень хорошо сочетаются. Нашподкласс PriorityQueue можно, например, обобщить следующим образом:template<class Item>class PriorityQueue : public Queue<Item> {public:PriorityQueue();virtual ~PriorityQueue();virtual void add(const Item&);…};Безопасность с точки зрения типов - ключевое преимущество данногоподхода.
Мы можем создать целый ряд различных классов конкретныхочередей:Queue<char> characterQueue;typedef Queue<MetworkEvent> EventQueue;typedef PriorityQueue<NetworkEvent> PriorityEventQueue;Рис. 9-1. Наследование и параметризацияПри этом язык реализации не позволит нам присоединить событие кочереди символов, а вещественное число - к очереди событий.Рис. 9-1 иллюстрирует отношения между параметризованным классом(Queue), его подклассом (PriorityQueue), примером этого подкласса(PriorityEventQueue) и одним из его экземпляров (mailQueue).Этот пример подтверждает правильность одного из самых первыхнаших архитектурных решений: почти все классы нашей библиотеки должныбыть параметризованными.
Тогда будет выполнено и требованиезащищенности.МакроорганизацияКак уже отмечалось в предыдущих главах, классы есть необходимое,но не достаточное средство декомпозиции системы. Это замечание в полноймере касается и библиотеки классов. Неупорядоченный набор классов, вкотором разработчики копаются в поисках чего-либо полезного, - едва ли нехудшее из возможных решений. Лучше разбить классы на отдельныекатегории (рис. 9-2). Такое решение позволяет удовлетворить требованиюпростоты библиотеки.При первом взгляде на проблемную область легко заметить, что мымогли бы воспользоваться общими функциональными свойствами классов.Поэтому заведем общедоступную категорию Support (поддержка) дляабстракций низкого уровня и классов, поддерживающих общие механизмыбиблиотеки.Это наблюдение приводит нас ко второму принципу архитектурыбиблиотеки: четкое разделение между политикой и реализацией.
Такиеабстракции, как очереди, множества и кольца, отражают политикуиспользования низкоуровневых структур: связных списков или массивов.Очередь, например, выражает политику, при которой можно только удалятьэлементы из начала структуры и добавлять элементы к ее концу. Множество, сдругой стороны, не представляет никакой политики, требующей упорядоченияэлементов.
Кольцо требует упорядочения, но предполагает, что начальный иконечный элемент соединены. К категории Support мы будем относитьпростые абстракции - те, над которыми надстраивается политика.Поместив эту категорию классов в код библиотеки, мы поддерживаембиблиотечное требование расширяемости. Основная масса разработчиков,может быть, и не будет использовать классы из Support.
Однакоразработчики библиотек и болееРис. 9-2. Категории классов в библиотекепродвинутые программисты смогут задействовать базовые абстракции изSupport для конструирования новых классов или модификации поведениясуществующих.Как видно из рис. 9-2, библиотека организована не в виде дерева, а ввиде леса классов; здесь не существует единого базового класса, как этоготребуют языки типа Smalltalk.На рисунке этого не видно, но на самом деле классы категорийGraphs, Lists и Trees несколько отличаются от других структурныхклассов. Еще раньше мы отмечали, что абстракции типа деки и стека являютсямонолитными.
С монолитной структурой можно иметь дело только как сединым целым: ее нельзя разбить на отдельные идентифицируемыекомпоненты, и таким образом гарантируется ссылочная целостность. С другойстороны, в композитной структуре (такой как граф) структурное разделениедопускается. В ней мы можем, например, получать доступ к подспискам,ветвям дерева, отдельным вершинам или ребрам графа. Фундаментальноеразличие между этими двумя категориями структур лежит в семантикеопераций копирования, присваивания и сравнения.