Выбрать главу

Рассмотрим абстракцию различных типов емкостей, которые могут использоваться в нашей теплице. Вероятно, в ней есть емкости для воды и для минеральных удобрений; хотя первые предназначены для жидкостей, а вторые для сыпучих веществ, они имеют достаточно много общего, чтобы устроить иерархию классов. Начнем с типов.

// Число, обозначающее уровень от 0 до 100 процентов typedef float Level;

Операторы typedef в C++ не вводят новых типов. В частности, и Level и Concentration - на самом деле другие названия для float, и их можно свободно смешивать в вычислениях. В этом смысле C++ имеет слабую типизацию: значения примитивных типов, таких, как int или float неразличимы в пределах данного типа. Напротив, Ada и Object Pascal предоставляют сильную типизацию для примитивных типов. В Ada можно объявить самостоятельным типом интервал значений или подмножество с ограниченной точностью.  

Строгая типизация предотвращает смешивание абстракций.

Построим теперь иерархию классов для емкостей:

class StorageTank { public:

StorageTank(); virtual ~StorageTank(); virtual void fill(); virtual void startDraining(); virtual void stopDraining(); Boolean isEmpty() const; Level level() const;

protected: ... };

class WaterTank : public StorageTank{ public:

WaterTank(); virtual ~WaterTank(); virtual void fill(); virtual void startDraining(); virtual void stopDraining(); void startHeating(); void stopHeating(); Temperature currentTemperature() const;

protected: ... };

class NutrientTank : public StorageTank { public:

NutrientTank(); virtual ~NutrientTank(); virtual void startDrainingt(); virtual void stopDraining();

protected: ... };

Класс StorageTank - это базовый класс иерархии. Он обеспечивает структуру и поведение общие для всех емкостей: возможность их наполнять или опустошать. Классы WaterTank (емкость для воды) и NutrientTank (для удобрений) наследуют свойства StorageTank, частично переопределяют их и добавляют кое-что свое: например, класс WaterTank вводит новое поведение, связанное с температурой.

Предположим, что мы имеем следующие описания:

StorageTank s1, s2; WaterTank w; NutrientTank n;

Заметьте, переменные такие как s1, s2, w или n - это не экземпляры соответствующих классов. На самом деле, это просто имена, которыми мы обозначаем объекты соответствующих классов: когда мы говорим "объект s1" мы на самом деле имеем ввиду экземпляр StorageTank, обозначаемый переменной s1. Мы вернемся к этому тонкому вопросу в следующей главе.

При проверке типов у классов, C++ типизирован гораздо строже. Под этим понимается, что выражения, содержащие вызовы операций, проверяются на согласование типов во время компиляции. Например, следующее правильно:

Level l = s1.level(); w.startDrainingt(); n.stopDraining();

Действительно, такие селекторы есть в классах, к которым принадлежат соответствующие переменные. Напротив, следующее неправильно и вызовет ошибку компиляции:

s1.startHeating(); // Неправильно n.stopHeating(); // Неправильно

Таких функций нет ни в самих классах, ни в их суперклассах. Но следующее

n.fill();

совершенно правильно: функции fill нет в определении NutrientTank, но она есть в вышестоящем классе.

Итак, сильная типизация заставляет нас соблюдать правила использования абстракций, поэтому она тем полезнее, чем больше проект. Однако у нее есть и теневая сторона. А именно, даже небольшие изменения в интерфейсе класса требуют перекомпиляции всех его подклассов. Кроме того, не имея параметризованных классов, о которых речь пойдет в главах 3 и 9, трудно представить себе, как можно было бы создать собрание разнородных объектов. Предположим, что мы хотим ввести абстракцию инвентарного списка, в котором собирается все имущество, связанное с теплицей. Обычная для С идиома применима и в C++: нужно использовать класс-контейнер, содержащий указатели на void, то есть на объекты произвольного типа.

class Inventory { public:

Inventory(); ~Inventory(); void add(void*); void remove(void*); void* mostRecent() const; void apply(Boolean (*)(void*));

private: ... };

Операция apply - это так называемый итератор, который позволяет применить какую-либо операцию ко всем объектам в списке. Подробнее об итераторах см. в следующей главе.