Квинтэссенция ООП

Публиковал этот текст уже не раз (три раза на разных ресурсах), но как мне кажется здесь он тоже будет интересен.

Вопрос “что такое Объектно–Ориентированное Программирование” — один из самых спорных у программистов. Много было сказано и написано умных слов про инкапсуляцию, полиморфизм, образ мышления, декомпозицию на сущности и так далее и тому подобное.
Тем не менее я снова всколыхну эту тему. Цель моя тут будет доказать и показать, что центральная идея ООП легко выражается одной фразой и по сути является одним несложным приёмом программирования. Причем эта суть постоянно проговаривается во всех книгах и рассуждениях про ООП, но она всегда замылена прочими не такими важными деталями и пространными рассуждениями. Но чтобы вычленить здесь эту суть нам надо будет отследить основные вехи эволюции императивных языков программирования.
Постоянно будет задаваться один и тот же вопрос — как то или иное направление в программировании это самое программирование должно было облегчить и упростить.

Неструктурированное программирование — раздаём имена
Первые компьютеры программировались прямо машинными кодами — это было жутко неудобно, поэтому появились ассемблеры — “человекочитаемый вид” машинного кода, способный к автоматическому выправлению адресов при изменении программы — чтобы это стало возможно программисты ввели понятие идентификаторов — именованных ячеек памяти, переменных и процедур. Довольно точно эту концепцию позаимствовали первые 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.
Забавно всё это.

2 лайка

Как-то всё у Вас просто и прямолинейно. Так не бывает. история языков гораздо богаче интереснее.

Ну так сабж не про историю, а про квинтэссенцию - центральную идею. И она вот да, несложна - если в развитом процедурном программировании с модулями мы делали заранее известные сущности с которыми работал многократно переиспользуемый код в виде процедур и функций, а детали были упрятаны в приватные части модулей, то в ООП добавилась только одна центральная идея: эти самые сущности научились быть полиморфными - модуль работающий со фруктами стал способен обрабатывать и яблоки и груши и мандарины, причём даже заранее не зная об их существовании.

Так получилось, что основным языком который потянул ООП в массы стал C++ (и уже за ним появились Java и C# и прочие современные тренды), поэтому на мой взгляд сложилась дезориентирующая ситуация когда то как реализован ООП в C++ стало и идейным примативом повлияв и оставив отпечаток везде от томиков идеологов до статей на вики. Плюс еще возникло очень много информационного шума когда не каждый замечая суть начинал приплетать с боку ненужное.

Например есть “принцип подстановки Лисков” - прямо вот оформилась та идея, что наследника должно подставить в ссылку на базовый класс и программа должна отработать без ошибок - но… но ведь это же буквально как сказать “хорошие программы с ООП должны быть написаны с применением ООП”. Это вот показывает какой хаос в головах в итоге образовался.

Какие именно карты подход С++ тут смешал: на мой взгляд это возведение трёх столпов: “наследование, инкапсуляция, полиморфизм”.
Как я утверждаю главное тут - это полиморфизм. Всё остальное находится на вторых ролях.
Выше я показывал, что инкапсуляция - вообще не изобретение ООП, справедливо лишь то, что её зафиксировали в конструкциях языка.

А вот с наследованием вопрос глубокий. Я могу его раскатать конечно, но времени особо нет.
Я поэтому замечу этот вопрос заставил в Java и C# свернуть с дорожки C++ на другую тропу - на интерфейсы.
Интерфейсы - это как раз попытка концентрироваться не на свойствах объекта как при наследовании, а на том в какие алгоритмы его можно скормить. Это и есть тютелька ООП.
Когда мы концентрируемся на наследовании у нас часто начинают возникать ненужные для ООП вопросы - сколько ног у млекопитающего, сколько двигателей у летающего транспорта - потому что наследники будут наследовать данные, т.е. свойства объекта.
А это немного другое нежели задача которую ООП решает изначально.
ООП изначально - это можно ли скормить объект в алгоритм или нет.
ISerializable можно скормить в алгоритмы с IStream. IFruit можно скормить в алгоритмы с IFruitBag. IWidget можно запихать в IWidgetFrame и так далее - это просто расширение способностей алгоритмов обрабатывать более широкий и легко расширяемый круг сущностей нежели были в процедурном программировании. И это отличная штука так то.
При этом конечно же можно написать ООП программу не использующую этого трюка, но это будет всё равно что написать процедурную программу из одной процедуры.

1 лайк

Так без истории, Вы центральную идею не с того места вытащили, она не оттуда растёт.

Отчего же: Dr. Alan Kay on the Meaning of "Object-Oriented Programming"

Historically, it’s worth looking at the USAF
Burroughs 220 file system (that I described in the Smalltalk
history), the early work of Doug Ross at MIT (AED and earlier) in
which he advocated embedding procedure pointers in data structures,
Sketchpad (which had full polymorphism – where e.g. the same offset
in its data structure meant “display” and there would be a pointer to
the appropriate routine for the type of object that structure
represented, etc., and the Burroughs B5000, whose program reference
tables were true “big objects” and contained pointers to both “data”
and “procedures” but could often do the right thing if it was trying
to go after data and found a procedure pointer. And the very first
problems I solved with my early Utah stuff was the “disappearing of
data” using only methods and objects.

Исторически стоит взглянуть на файловую системк USAF
Burroughs 220 (которую я описывал в Истории Smalltalk),
раннюю работу Doug Ross в MIT (AED и ранее) в которой
он отстаивал внедрение указателей на процедуры в структуры данных,
Sketchpad (который имел полный полиморфизм - где, например
одно и то же смещение в структуре данных означало “отобразить” и там
был указатель на соответствующую процедуру для типа объекта
которую структура представляла и т.д.
и Burroughs B5000, чьи таблицы на ссылок на программы
были настоящими “большими объектами” и содержали
указатели и на данные и на процедуры, но часто могли сделать всё
правильно если пытаться достать данные, но найти указатель
на процедуру.
И самые первые проблемы которые я решил в моих ранних
работах в Utah это “исчезновение данных” - использование
только методов и объектов.

Это Алана Кэя - основоположника ООП. Причём он сам то потащил идею гораздо дальше в асинхронный обмен сообщениями в Smalltalk, но это вот как раз в массы не пошло (если не считать WinAPI с PostMessage - это вот как раз очень массово используется и по сей день), но модель C++ она по большей части именно про внедрение указателей на процедуры в данные.

И? Алан Кей вовсе не был основоположником ООП. Да, он один из пионеров этого дела. Да, он разработчик ОО-языка (не первого, кстати), но вовсе не основоположник. Идеи генерировал не он. А его утверждение об “исчезновении данных” настолько спорно (и настолько часто оспаривалось) что и говорить неприлично. Собственно именно эта ориентация на “методы и объекты” и не дала смолтоку вырасти в “большой язык” потому, что он развивался в сторону противоположную заложенной в него идеологии.

Но, давайте не будем поднимать тут холивары тридцатилетней давности. Я лишь отметил, что точка зрения, которую Вы подаёте как истину, по меньшей мере спорна, не принимается многими, в том числе и мною.

2 лайка

если я правильно понял вашу идею - “тру ООП” это наследование не свойств обьекта, а его поведения.
То есть если у нас есть класс “сущность” и метод “ползать” - то нам не важно. сколько ног у обьекта и как оно “ползает” - может это мокрица с 40 ногами, казак-пластун с двумя , или змея вообще без ног…

Это очень интересная концепция (по крайней мере для меня). не думал про ООП с этой точки зрения…

А вот капля по стеклу тоже “ползет” - можно ли ее встроить в этот интерфейс? :slight_smile:

Тут уже наверное лучше привести конкретный пример.
Вот выше упоминался Sketchpad. Используем как пример редактор текста с картинками - с очень для начала простой идеей - документ это коллекция параграфов текста перемежающихся картинками в любом порядке.
Как бы мы запроектировали такую программу в процедурном стиле? Влобная реализация - сделать коллекцию параграфов с полем type который либо text либо image и при выводе и обработке нажатий мышкой и кнопками в активном параграфе срабатывают многочисленные switch ( type ) с тем чтобы в тексте курсор перемещать либо картинку влево-вправо двигать от центра.
И это уже назревающая проблема - если заказчик захочет добавить третий тип параграфа - pie_chart (круговая диаграмма), то нам придётся перешерстить кучу switch-ей и легко забыть где и что надо исправлять дополнять и заменять при модификациях программы.
Такие дизайны довольно быстро начинают превращаться в кодерский ад.

Поэтому заметим, что на самом деле интерфейсную часть программы не особо сильно интересует какой там тип у подлежащего параграфа - с её точки зрения - ей бы вот просто “сообщить” текущему параграфу, что в нём нажали кнопку “курсор влево” или пытаются ввести печатный символ с клавиатуры, а параграф как бы там сам разрулил что в этом случае надо сделать.
И вот тут то и появляется ООП - что если мы все параграфы сделаем одинаковыми перед лицом остальных алгоритмов - все параграфы просто как то реагируют на события - на сообщения - мы говорим параграфу “а вот тут нажали кнопку влево” - и если это параграф текста он двигает курсор влево, а если параграф с картинкой, то двигает саму картинку влево от центра.
Сам параграф содержит алгоритм того что делать при наступлении этого события - и нужно сделать интерфейс позволяющий закидывать к конкретный параграф одни и те же события чтобы они разруливались разными образами в зависимости от того как сам параграф видит нужной обрабатывать это событие.

Итак мы преобразуем программу из switch case в сообщения “параграф, нарисуйся на куске холста по таким то координатам”, “параграф, отреагируй на ввод буквы с клавиатуры” и так далее. Интерфейс параграфа - это реакции на события и событийные поведения.

Поэтому всё верно - нужно концентрироваться не на свойствах объекта, а на его поведении - на том какое действие от него можно затребовать или о каком событии ему можно сообщить. Всё так.
Хотя требовать данные от объекта всё-таки бывает полезно, но делать это тоже надо через методы чтобы в любом случае была прослойка которую в будущем можно будет безболезненно поменять не затрагивая интерфейс.
Но максимально идеально выходить именно на событийно-поведенческую модель взаимодействия объектов.
Например в компьютерной игре взрывается бочка и раздаёт окружающим объектам могущим принимать урон сообщение “прими урон в 200 единиц”.
А дальше сами объекты решают к чему это приведёт - железная бочка не примет урона меньше 300 единиц вообще, деревянная бочка уменьшит прочность и если она окажется меньше нуля, то пометится к удалению и породит ворох деревянных досок вместо себя.
Последний этот пример на самом деле не очень качественный, т.к. симуляция игровых миров это прям сложнейшая тема и можно жёстко придираться к каждому слову и понятию, но уже на примере редактора текста думаю понятно откуда и зачем дует ветер ООП.

Простите что влезаю в разговор, но ИМХО это скорее о Протокольно Ориентированном П.
Типа Свифта.

1 лайк

Да мы только рады, но нет - это именно про ООП.
Еще один хрестоматийный пример применения ООП - это потоки ввода-вывода.
Есть базовый класс Stream олицетворяющий какую то сущность из которой можно читать и в которую можно писать поток байт и есть базовый класс Serializable который можно записать или прочитать из этого самого Stream.
А далее все пути открыты - объект отнаследованный от Serializabe можно прочитать/записать как в файл (FileStream) так и в память (MemoryStream) так и в сеть (SocketStream) - и код будет абсолютно одинаков - object.writeToStream(stream) - и сам то объект может быть вообще чем угодно - неважно - одна строчка кода будет прекрасно работать как с различными объектами так и с различными видами потоков ввода-вывода.
Истинное торжество ООП!
И действительно мы тут “общаемся” с объектами глаголами - запишись, прочитайся, а не пытаемся из них получить данные “дай мне массив байт чтобы дальше я сам решил как его записать в поток” - второе это вредная концепция и плохой ООП.

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

1 лайк

Резюме такое, что для простых микроконтроллерных задач эта мозготня только вредит - не найдешь что где глючит.

2 лайка

И не для простых тоже :smile:

1 лайк

Очень верное замечание.
Не нужны эти все полиморфные сложности если проект не перерастает некотороую границу по сложности.
До тех пор вполне правильно обходится обычным структурно-модульным программированием без прикрас.
Сложность требующая вовлечение ООП я даже не представляю как может возникнуть на устройстве с 2Кб ОЗУ.

Я с уважением отношусь ко всем этим СРР и упрятыванием всего в загадочные классы, но таки думаю что они нужны и необходимы когда проект огромный и над ним работают тыщщи программеров. Типа винды и Индии ахаха. Там без этого никак.
А в нашем селе проще писать на Си с тупыми глобальными переменными и гоуту.

Это кому как.

1 лайк

Причем тут сложность проекта? Ооп это просто один из вариантов проектирования кода. Тому кто привык работать с классами и объектами, даже простейший блинк проще будет написать в парадигме ООП, чем в виде отдельных процедур.
Первое что создаём - класс светодиода. Потом методы инит и сеттер для периода. И метод блинк. Вот и все.
И чем тут помешает ваши пресловутые 2к ОЗУ?

1 лайк

Вполне согласен. Только давайте будем честны перед самими собой. Каждому “проще” писать на том, что он лучше знает. И ваше замечание о том, что вам проще без ООП - это ведь не потому что ООП плохой, а просто потому, что вы его не знаете…ну как минимум не знаете так, как обычный процедурный С

а если заглючит или зависнет - где концы искать?

А где вы их ищете в “обычном Си”? методы отладки для обычного Си или ООП ничем не отличаются