
Онлайн книга «Чистая архитектура. Искусство разработки программного обеспечения»
Языки Java и C# полностью отменили деление на заголовок/реализацию, ослабив инкапсуляцию еще больше. В этих языках невозможно разделить объявление и определение класса. По описанным причинам трудно согласиться, что ОО зависит от строгой инкапсуляции. В действительности многие языки ОО практически не имеют принудительной инкапсуляции [13]. ОО безусловно полагается на поведение программистов — что они не станут использовать обходные приемы для работы с инкапсулированными данными. То есть языки, заявляющие о поддержке OO, фактически ослабили превосходную инкапсуляцию, некогда существовавшую в C. Наследование?
Языки ОО не улучшили инкапсуляцию, зато они дали нам наследование. Точнее — ее разновидность. По сути, наследование — это всего лишь повторное объявление группы переменных и функций в ограниченной области видимости. Нечто похожее программисты на C проделывали вручную задолго до появления языков ОО [14]. Взгляните на дополнение к нашей исходной программе point.h на языке C: namedPoint.h
namedPoint.c
main.c
Внимательно рассмотрев основной код в файле main.c, можно заметить, что структура данных NamedPoint используется, как если бы она была производной от структуры Point. Такое оказалось возможным потому, что первые два поля в NamedPoint совпадают с полями в Point. Проще говоря, NamedPoint может маскироваться под Point, потому что NamedPoint фактически является надмножеством Point и имеет члены, соответствующие структуре Point, следующие в том же порядке. Этот прием широко применялся [15] программистами до появления ОО. Фактически именно так C++ реализует единственное наследование. То есть можно сказать, что некоторая разновидность наследования у нас имелась задолго до появления языков ОО. Впрочем, это утверждение не совсем истинно. У нас имелся трюк, хитрость, не настолько удобный, как настоящее наследование. Кроме того, с помощью описанного приема очень сложно получить что-то похожее на множественное наследование. Обратите также внимание, как в main.c мне пришлось приводить аргументы NamedPoint к типу Point. В настоящем языке ОО такое приведение к родительскому типу производится неявно. Справедливости ради следует отметить, что языки ОО действительно сделали маскировку структур данных более удобной, хотя это и не совсем новая особенность. Итак, мы не можем дать идее ОО ни одного очка за инкапсуляцию и можем дать лишь пол-очка за наследование. Пока что общий счет не впечатляет. Но у нас есть еще одно понятие. Полиморфизм?
Была ли возможность реализовать полиморфное поведение до появления языков ОО? Конечно! Взгляните на следующую простую программу copy на языке C.
Функция getchar() читает символы из STDIN. Но какое устройство в действительности скрыто за ширмой STDIN? Функция putchar() записывает символы в устройство STDOUT. Но что это за устройство? Эти функции являются полиморфными — их поведение зависит от типов устройств STDIN и STDOUT. В некотором смысле STDIN и STDOUT похожи на интерфейсы в силе Java, когда для каждого устройства имеется своя реализация этих интерфейсов. Конечно, в примере программы на C нет никаких интерфейсов, но как тогда вызов getchar() передается драйверу устройства, который фактически читает символ? Ответ на этот вопрос прост: операционная система UNIX требует, чтобы каждый драйвер устройства ввода/вывода реализовал пять стандартных функций [16]: open, close, read, write и seek. Сигнатуры этих функций должны совпадать для всех драйверов. Структура FILE имеет пять указателей на функции. В нашем случае она могла бы выглядеть как-то так:
|