logo
Языки программирования

В каждом объекте должно скрываться одно важное проектное решение.

Очень полезно бывает задать себе вопрос: «возможно ли, что это решение изменится за время жизни программы?»

Конкретные дисплейные терминалы и принтеры, выбранные для системы предварительных заказов, явно подлежат обновлению. Точно так же решения по организации базы данных, вероятно, будут изменяться, чтобы улучшить эффективность, поскольку система растет. С другой стороны, можно было бы привести доводы, что изменение формы данных заказчика маловероятно и что отдельный объект здесь не нужен. Даже если вы не согласны с нашим про­ектным решением создать объект Заказчик, вы должны согласиться, что объ­ектно-ориентированное проектирование — хороший общий подход для об­суждения проблем разработки и достоинств одного проекта перед другим.

В следующих разделах языковая поддержка ООП будет обсуждаться на при­мере двух языков: C++ и Ada 95. Сначала мы рассмотрим язык C++, который был разработан как добавление одной интегрированной конструкции для ООП к языку С, в котором нет поддержки даже для модулей. Затем мы увидим, как полное объектно-ориентированное программирование определено в язы­ке Ada 95 путем добавления нескольких небольших конструкций к языку Ada 83, который уже имел много свойств, частично поддерживающих ООП.

14.2. Объектно-ориентированное программирование на языке C++

Говорят, что язык программирования поддерживает ООП, если он включает конструкции для:

• инкапсуляции и абстракции данных,

• наследования,

• динамического полиморфизма.

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

Такие модули, как пакеты в языке Ada, инкапсулируют вычислительные ресурсы, выставляя только спецификацию интерфейса. Абстракция данных может быть достигнута через определение представления данных в закрытой части, к которой нельзя обращаться из других единиц. Единица инкапсуля­ции и абстракции в языке C++ — это класс (class), который содержит объявления подпрограмм и типов данных. Из класса создаются фактические объек­ты, называемые экземлярами(instances). Пример класса в языке C++:

class Airplane_Data {

public:

char *get_id(char *s) const {return id;}

void set_id(char *s) {strcpy(id, s);}

int get_speed() const {return speed;}

void set_speed(int i) {speed=i;}

int get_altitude() const {return altitude;}

void set_altitude(int i) {altitude = i;}

private:

char id[80];

int speed;

int altitude;

};

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

class Airplanes {

public:

void New_Airplane(Airplane_Data, int &);

void Get_Airplane(int, Airplane_Data &) const;

private:

Airplane_Data database[100];

int current_airplanes;

int find_empty_entry();

};

Каждый класс разрабатывается для того, чтобы инкапсулировать набор объ­явлений данных. Объявления данных в закрытой части могут быть изменены без изменения программ, использующих этот класс и называющихся клиен­тами (clients) класса, хотя их и придется перекомпилировать. Класс имеет на­бор интерфейсных функций, которые извлекают и обновляют значения дан­ных, внутренних по отношению к классу.

Вы можете задать вопрос, почему Airplane_Data лучше сделать отдельным классом, а не просто объявить обычной общей (public) записью. Это спорное проектное решение: данные должны быть скрыты в классе, если вы полагае­те, что внутреннее представление может измениться. Например, вы можете знать, что один заказчик предпочитает измерять высоту в английских футах, тогда как другой предпочитает метры. Определяя отдельный класс для

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

За эту гибкость приходится платить определенную цену; каждый доступ к значению данных требует вызова подпрограммы:

Aircraft_Data a; // Экземпляр класса

int alt;

alt = a.get_altitud(e); // Получить значение, скрытое в экземпляре

alt = (alt* 2)+ 1000;

a.set_altitude(alt); // Вернуть значение в экземпляр

вместо простого оператора присваивания в случае, когда а общая (public) за­пись:

a.alt = (a.alt*2) + 1000;

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

Однако инкапсуляция вовсе не обязана сопровождаться значительными затратами времени выполнения. Как показано в примере, тело интерфейс­ной функции может быть написано внутри объявления класса; в этом случае функция является подставляемой (встраиваемой, inline) функцией, т.е. не ис­пользуется механизм вызова подпрограммы и возврата из нее (см. гл. 7). Вместо этого код тела подпрограммы вставляется непосредственно внутрь последовательности кода в точке вызова. Поскольку при подстановке функции мы расплачиваемся пространством за время, подпрограммы должны быть очень маленькими (не более двух или трех команд). Другой фактор, который следует рассмотреть перед подстановкой подпрограммы, это то, что она вводит дополнительные условия для компиляции. Если вы изменяете подставляемую подпрограмму, все клиенты должна быть пере­компилированы.