Публиковал этот текст уже не раз (три раза на разных ресурсах), но как мне кажется здесь он тоже будет интересен.
Вопрос “что такое Объектно–Ориентированное Программирование” — один из самых спорных у программистов. Много было сказано и написано умных слов про инкапсуляцию, полиморфизм, образ мышления, декомпозицию на сущности и так далее и тому подобное.
Тем не менее я снова всколыхну эту тему. Цель моя тут будет доказать и показать, что центральная идея ООП легко выражается одной фразой и по сути является одним несложным приёмом программирования. Причем эта суть постоянно проговаривается во всех книгах и рассуждениях про ООП, но она всегда замылена прочими не такими важными деталями и пространными рассуждениями. Но чтобы вычленить здесь эту суть нам надо будет отследить основные вехи эволюции императивных языков программирования.
Постоянно будет задаваться один и тот же вопрос — как то или иное направление в программировании это самое программирование должно было облегчить и упростить.
Неструктурированное программирование — раздаём имена
Первые компьютеры программировались прямо машинными кодами — это было жутко неудобно, поэтому появились ассемблеры — “человекочитаемый вид” машинного кода, способный к автоматическому выправлению адресов при изменении программы — чтобы это стало возможно программисты ввели понятие идентификаторов — именованных ячеек памяти, переменных и процедур. Довольно точно эту концепцию позаимствовали первые 8–битные интерпретаторы Бейсика — переменные получили имена, а перенумерация строк автоматически выправляла множественные GOTO 1010. Так что квинтэссенцию этого вклада можно выразить как “приводим к человекочитаемому виду и раздаём имена”. Тем не менее программирование было довольно хаотичным, а программы получались запутанными.
Структурированное программирование — делим на блоки
Хаос из GOTO у многих засел в самых печенках и структурное дробление программы на вложенные блоки с хорошей читаемостью и без лапши GOTO стало для них настоящим свершением. Тут всё довольно очевидно — “убираем лапшу из GOTO и делим код на блоки”.
Процедурное программирование и сложные структуры данных — блочность кода и данных
Данное упрощение в целом неотделимо от структурного программирования — в программах появляются мощные блоки процедур/функций, которые можно написать один раз и многократно использовать с элегантностью недоступной GOSUB из 8–битных Бейсиков. Но по настоящему процедуры начинают быть полезными когда мы их сочетаем с блоками данных — структурами и прочими “составными” типами произвольной сложности. Например определив структуру комплексного числа можно было написать процедуры по арифметике над экземплярами таких структур и пользоваться и теми и другими многократно. Более того — изменив определение структуры и точечно поправив набор работающих с ней процедур стало возможным резко сократить сложность модификации программы — главное правильно разбросать сущности по структурам и действия с ними по процедурам. Девиз можно выразить так: “пишем один раз код работающий с определенной сущностью и многократно его переиспользуем”. Но чтобы довернуть гайки удобства модификации этих сущностей нужно было совершить еще одно действие:
Модульное программирование — прячем детали и выпячиваем важное
Если теперь структуру комплексного числа с набором работающих с ней процедур и функций изолировать от остальной программы в модуле система приобретает совсем удобный вид. Если вы захотите исправить или проапгрейдить сущность комплексного числа — вы уже даже заранее знаете в какой файл нужно залезть и где надо эти исправления делать. Ничто в остальной программе не должно исправляться при этом, если всё сделать правильно. И вот тут программисты обнаружили одну простую вещь — совсем всё правильно и удобно получается, если не давать внешнему коду за пределами модуля вообще лазить внутрь данных структуры, а всю работу с ней проводить через процедуры/функции. Представителей этой идиомы можно встретить очень часто. Классический пример — указатель на структуру FILE из стандартной библиотеки языка Си. Вы делаете fopen, вы делаете fwrite и fclose, но никогда не лезете грязными ручонками в саму эту структуру — большинство даже не подозревает какие поля в ней находятся.
А ведь это называется инкапсуляция и нам постоянно говорят, что это один из столпов ООП. Так может мы уже добрались здесь до ООП? Нет, мы всё еще имеем дело с процедурным программированием обогащённым модулями. Настоящих объектов здесь еще нет — FILE это структура.
Основная мысль данной эволюции остаётся почти прежней: “пишем код работающий с определенными сущностями, тщательно упрятывая данные за кодом”.
ООП
В процессе дальнейшего программирования в языках с указателями на функции (или процедурными типами) был замечен один крайне полезный трюк — если в структуру положить собственно указатель на функцию, то её функционал можно значительно расширить, даже без модификации модуля где эта структура объявлена и где определены процедуры по работе с ней.
Вот именно этот трюк и стал рождением ООП — наша сущность выделенная в структуру стала не просто черным ящиком с заранее определенным поведением, но она превратилась в чёрный ящик с заранее не определенным поведением!
Центральная идея ООП тогда выражается по аналогии с процедурной парадигмой заменой всего одного слова: “пишем один раз код работающий с заранее неизвестной сущностью и многократно его переиспользуем”.
В языках со строгой типизацией “неизвестность” объекта выражается в том, что в код работающий с базовым классом можно передавать любых его наследников. А клей, заставляющий код наследников перехватывать выполнение кода родителя — это виртуальные методы (в некоторых языках все являются таковыми). В языках без строгой типизации всё может быть еще проще — в объект прямо так и помещаются ссылки на функции как в JavaScript.
То есть главным свойством и столпом ООП является полиморфизм (динамическая диспетчеризация) — способность объектов вести себя разным образом при работе с уже написанным кодом. Чистые процедуры со структурами такого не умели.
При этом механизм самого полиморфизма в сущности не играет роли. На ООП можно писать даже если язык прямо не поддерживает данной парадигмы. Например любой кто пишет GUI почти неминуемо обречен написать объектную систему. Взять, например, WinAPI по части работы с окнами — хендлы окон и отправка сообщений разнообразного состава им через PostMessage — это самые настоящие объекты с самым настоящим механизмом динамической диспетчеризации.
С другой стороны строка программы вида “some.method(…)” не означает, что some это обязательно объект в смысле ООП. Это может быть обычная структура из процедурного программирования, обмазанная синтаксическим сахаром упрятывания процедуры в имя структуры — пока нет виртуального метода — нет и объекта (в смысле ООП, хотя можно говорить в иных смыслах — “утилитарный объект” и так далее).
Здесь приведу слова пионера ООП — Алана Кэя (создателя Smalltalk):
“Мне жаль, что давным давно я использовал термин «объект» для этой темы, потому что из–за этого многие люди фокусируются на меньшей из идей. Большая идея — это «сообщения»”
“ООП для меня это сообщения, локальное удержание и защита, скрытие состояния и позднее связывание всего.”
Итог
Итак, если подытожить, то классическое программирование до ООП можно выразить фразой:
Пишем и повторно используем код работающий с известными сущностями.
А ООП меняет это на:
Пишем и повторно используем код работающий с заранее неизвестными сущностями.
В этом вся эволюция и весь прогресс и только лишь - только применение виртуальных методов в программе на C++ делает её ООП-нутой, но что еще поразительнее - даже без виртуальных методов на Си применение самописаного механизма динамической диспетчеризации тоже может сделать ООП-нутую программу по архитектуре. Таковые, например, получались давным давно у Джона Кармака который писал игры Doom и Quake вплость до Doom 3 где он наконец то начал использовать ключевое слово class.
Забавно всё это.