Страница 11 из 20
void write(char c) {/*…*/}
void seek(long index, int mode) {/*…*/}
struct FILE console = {open, close, read, write, seek};
Если теперь предположить, что символ STDIN определен как указатель FILE* и ссылается на структуру console, тогда getchar() можно реализовать как-то так:
extern struct FILE* STDIN;
int getchar() {
return STDIN->read();
}
Иными словами, getchar() просто вызывает функцию, на которую ссылается указатель read в структуре FILE, на которую, в свою очередь, ссылается STDIN.
Этот простой трюк составляет основу полиморфизма в ОО. В C++, например, каждая виртуальная функция в классе представлена указателем в таблице виртуальных методов vtable и все вызовы виртуальных функций выполняются через эту таблицу. Конструкторы производных классов просто инициализируют таблицу vtable объекта указателями на свои версии функций.
Суть полиморфизма заключается в применении указателей на функции. Программисты использовали указатели на функции для достижения полиморфного поведения еще со времен появления архитектуры фон Неймана в конце 1940-х годов. Иными словами, парадигма ОО не принесла ничего нового.
Впрочем, это не совсем верно. Пусть полиморфизм появился раньше языков ОО, но они сделали его намного надежнее и удобнее.
Проблема явного использования указателей на функции для создания полиморфного поведения в том, что указатели на функции по своей природе опасны. Такое их применение оговаривается множеством соглашений. Вы должны помнить об этих соглашениях и инициализировать указатели. Вы должны помнить об этих соглашениях и вызывать функции посредством указателей. Если какой-то программист забудет о соглашениях, возникшую в результате ошибку будет чертовски трудно отыскать и устранить.
Языки ОО избавляют от необходимости помнить об этих соглашениях и, соответственно, устраняют опасности, связанные с этим. Поддержка полиморфизма на уровне языка делает его использование тривиально простым. Это обстоятельство открывает новые возможности, о которых программисты на C могли только мечтать. Отсюда можно заключить, что ОО накладывает ограничение на косвенную передачу управления.
Сильные стороны полиморфизма
Какими положительными чертами обладает полиморфизм? Чтобы в полной мере оценить их, рассмотрим пример программы copy. Что случится с программой, если создать новое устройство ввода/вывода? Допустим, мы решили использовать программу copy для копирования данных из устройства распознавания рукописного текста в устройство синтеза речи: что нужно изменить в программе copy, чтобы она смогла работать с новыми устройствами?
Самое интересное, что никаких изменений не требуется! В действительности нам не придется даже перекомпилировать программу copy. Почему? Потому что исходный код программы copy не зависит от исходного кода драйверов ввода/вывода. Пока драйверы реализуют пять стандартных функций, определяемых структурой FILE, программа copy сможет с успехом их использовать.
Проще говоря, устройства ввода/вывода превратились в плагины для программы copy.
Почему операционная система UNIX превратила устройства ввода/вывода в плагины? Потому что в конце 1950-х годов мы поняли, что наши программы не должны зависеть от конкретных устройств. Почему? Потому что мы успели написать массу программ, зависящих от устройств, прежде чем смогли понять, что в действительности мы хотели бы, чтобы эти программы, выполняя свою работу, могли бы использовать разные устройства.
Например, раньше часто писались программы, читавшие исходные данные из пакета перфокарт[17] и пробивавшие на перфораторе новую стопку перфокарт с результатами. Позднее наши клиенты стали передавать исходные данные не на перфокартах, а на магнитных лентах. Это было неудобно, потому что приходилось переписывать большие фрагменты первоначальных программ. Было бы намного удобнее, если бы та же программа могла работать и с перфокартами, и с магнитной лентой.
Для поддержки независимости от устройств ввода/вывода была придумана архитектура плагинов и реализована практически во всех операционных системах. Но даже после этого большинство программистов не давали распространения этой идее в своих программах, потому что использование указателей на функции было опасно.
Объектно-ориентированная парадигма позволила использовать архитектуру плагинов повсеместно.
Инверсия зависимости
Представьте, на что походило программное обеспечение до появления надежного и удобного механизма полиморфизма. В типичном дереве вызовов главная функция вызывала функции верхнего уровня, которые вызывали функции среднего уровня, в свою очередь, вызывавшие функции нижнего уровня. Однако в таком дереве вызовов зависимости исходного кода непреклонно следовали за потоком управления (рис. 5.1).
Рис. 5.1. Зависимости исходного кода следуют за потоком управления
Чтобы вызвать одну из функций верхнего уровня, функция main должна сослаться на модуль, содержащий эту функцию. В языке C для этой цели используется директива #include. В Java – инструкция import. В C# – инструкция using. В действительности любой вызывающий код был вынужден ссылаться на модуль, содержащий вызываемый код.
Эти требования предоставляли архитектору программного обеспечения несколько вариантов. Поток управления определяется поведением системы, а зависимости исходного кода определяются этим потоком управления.
Однако когда в игру включился полиморфизм, стало возможным нечто совершенно иное (рис. 5.2).
Рис. 5.2. Инверсия зависимости
На рис. 5.2 модуль верхнего уровня HL1 вызывает функцию F() из модуля среднего уровня ML1. Вызов посредством интерфейса является уловкой лишь для исходного кода. Во время выполнения интерфейсов не существует. Модуль HL1 просто вызывает F() внутри ML1[18].
Но обратите внимание, что направление зависимости в исходном коде (отношение наследования) между ML1 и интерфейсом I поменялось на противоположное по отношению к потоку управления. Этот эффект называют инверсией зависимости (dependency inversion), и он имеет далеко идущие последствия для архитекторов программного обеспечения.
17
Перфокарты IBM Hollerith имели ширину 80 колонок. Я уверен, что многие из вас никогда даже не видели их, но они широко были распространены в 1950-е, 1960-е и даже в 1970-е годы.
18
Хотя и косвенно.