
Онлайн книга «Чистая архитектура. Искусство разработки программного обеспечения»
Драйвер консоли определяет эти функции и инициализирует указатели на них в структуре FILE примерно так:
Если теперь предположить, что символ STDIN определен как указатель FILE* и ссылается на структуру console, тогда getchar() можно реализовать как-то так:
Иными словами, 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. Инверсия зависимости |