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).

Чистая архитектура. Искусство разработки программного обеспечения - i_015.jpg

Рис. 5.1. Зависимости исходного кода следуют за потоком управления

Чтобы вызвать одну из функций верхнего уровня, функция main должна сослаться на модуль, содержащий эту функцию. В языке C для этой цели используется директива

#include
. В Java – инструкция
import
. В C# – инструкция
using
. В действительности любой вызывающий код был вынужден ссылаться на модуль, содержащий вызываемый код.

Эти требования предоставляли архитектору программного обеспечения несколько вариантов. Поток управления определяется поведением системы, а зависимости исходного кода определяются этим потоком управления.

Однако когда в игру включился полиморфизм, стало возможным нечто совершенно иное (рис. 5.2).

Чистая архитектура. Искусство разработки программного обеспечения - i_016.jpg

Рис. 5.2. Инверсия зависимости

На рис. 5.2 модуль верхнего уровня

HL1
вызывает функцию
F()
из модуля среднего уровня
ML1
. Вызов посредством интерфейса является уловкой лишь для исходного кода. Во время выполнения интерфейсов не существует. Модуль HL1 просто вызывает
F()
внутри
ML1
[18].

Но обратите внимание, что направление зависимости в исходном коде (отношение наследования) между

ML1
и интерфейсом
I
поменялось на противоположное по отношению к потоку управления. Этот эффект называют инверсией зависимости (dependency inversion), и он имеет далеко идущие последствия для архитекторов программного обеспечения.