Тактические вопросы
В соответствии с законом разработки программ Коггинса "прагматизм всегда должен быть предпочтительней элегантности, ведь Природу все равно ничем не удивить". Следствие: проектирование никогда не будет полностью независимым от языка реализации проекта. Особенности языка неизбежно наложат отпечаток на те или иные архитектурные решения, и их игнорирование может привести к тому, что нам придется работать в дальнейшем с абстракциями, не в полной мере учитывающими преимущества и недостатки конкретного языка реализации.
Как было отмечено в главе 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).
Этот пример подтверждает правильность одного из самых первых наших архитектурных решений: почти все классы нашей библиотеки должны быть параметризованными.Тогда будет выполнено и требование защищенности.