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

Тема в разделе "WASM.ARTICLES", создана пользователем aa_dav, 13 ноя 2023.

  1. aa_dav

    aa_dav Active Member

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

    Неструктурированное программирование — раздаём имена
    Первые компьютеры программировались прямо машинными кодами — это было жутко неудобно, поэтому появились ассемблеры — "человекочитаемый вид" машинного кода, способный к автоматическому выправлению адресов при изменении программы — чтобы это стало возможно программисты ввели понятие идентификаторов — именованных ячеек памяти, переменных и процедур. Довольно точно эту концепцию позаимствовали первые 8–битные интерпретаторы Бейсика — переменные получили имена, а перенумерация строк автоматически выправляла множественные GOTO 1010. Так что квинтэссенцию этого вклада можно выразить как "приводим к человекочитаемому виду и раздаём имена". Тем не менее программирование было довольно хаотичным, а программы получались запутанными.

    Структурированное программирование — делим на блоки
    Хаос из GOTO у многих засел в самых печенках и структурное дробление программы на вложенные блоки с хорошей читаемостью и без лапши GOTO стало для них настоящим свершением. Тут всё довольно очевидно — "убираем лапшу из GOTO и делим код на блоки".

    Процедурное программирование и сложные структуры данных — блочность кода и данных
    Данное упрощение в целом неотделимо от структурного программирования — в программах появляются мощные блоки процедур/функций, которые можно написать один раз и многократно использовать с элегантностью недоступной GOSUB из 8–битных Бейсиков. Но по настоящему процедуры начинают быть полезными когда мы их сочетаем с блоками данных — структурами и прочими "составными" типами произвольной сложности. Например определив структуру комплексного числа можно было написать процедуры по арифметике над экземплярами таких структур и пользоваться и теми и другими многократно. Более того — изменив определение структуры и точечно поправив набор работающих с ней процедур стало возможным резко сократить сложность модификации программы — главное правильно разбросать сущности по структурам и действия с ними по процедурам. Девиз можно выразить так: "пишем один раз код работающий с определенной сущностью и многократно его переиспользуем".
    Но чтобы довернуть гайки удобства модификации этих сущностей нужно было совершить еще одно действие:

    Модульное программирование — прячем детали и выпячиваем важное
    Если теперь структуру комплексного числа с набором работающих с ней процедур и функций изолировать от остальной программы в модуле система приобретает совсем удобный вид. Если вы захотите исправить или проапгрейдить сущность комплексного числа — вы уже даже заранее знаете в какой файл нужно залезть и где надо эти исправления делать. Ничто в остальной программе не должно исправляться при этом, если всё сделать правильно. И вот тут программисты обнаружили одну простую вещь — совсем всё правильно и удобно получается, если не давать внешнему коду за пределами модуля вообще лазить внутрь данных структуры, а всю работу с ней проводить через процедуры/функции. Представителей этой идиомы можно встретить очень часто. Классический пример — указатель на структуру FILE из стандартной библиотеки языка Си. Вы делаете fopen, вы делаете fwrite и fclose, но никогда не лезете грязными ручонками в саму эту структуру — большинство даже не подозревает какие поля в ней находятся.
    А ведь это называется инкапсуляция и нам постоянно говорят, что это один из столпов ООП. Так может мы уже добрались здесь до ООП? Нет, мы всё еще имеем дело с процедурным программированием обогащённым модулями. Настоящих объектов здесь еще нет — FILE это структура, а fopen - классика модульного программирования.
    Основная мысль данной эволюции остаётся почти прежней: "пишем код работающий с определенными сущностями, тщательно упрятывая данные за кодом".

    ООП
    В процессе дальнейшего программирования в языках с указателями на функции (или процедурными типами) был замечен один крайне полезный трюк — если в структуру положить собственно указатель на функцию, то можно через вызов такой "внедрённой" функции делегировать работу в какое то заранее неизвестное место и подставляя ссылки на объекты с разными указателями на функции заставлять работать один и тот же код разным расширяемым образом.
    Вот именно этот трюк и стал рождением ООП — наша сущность выделенная в структуру стала не просто черным ящиком с заранее определенным поведением, но она превратилась в чёрный ящик с заранее неопределенным поведением!
    Центральная идея ООП тогда выражается по аналогии с процедурной парадигмой заменой всего одного слова: "пишем один раз код работающий с заранее неизвестной сущностью и многократно его переиспользуем".
    В языках со строгой типизацией "неизвестность" объекта выражается в том, что в код работающий с базовым классом можно передавать любых его наследников. А клей, заставляющий код наследников перехватывать выполнение кода родителя — это виртуальные методы (в некоторых языках все являются таковыми). В языках без строгой типизации всё может быть еще проще — в объекты прямо так и помещаются ссылки на разные функции под одним и тем же именем как, например, в JavaScript.
    То есть главным свойством и столпом ООП является полиморфизм (динамическая диспетчеризация) — способность объектов вести себя разным образом при работе с уже написанным кодом. Чистые процедуры со структурами такого не умели.
    При этом механизм самого полиморфизма в сущности не играет роли. На ООП можно писать даже если язык прямо не поддерживает данной парадигмы. Например любой кто пишет GUI почти неминуемо обречен написать объектную систему. Взять, например, WinAPI по части работы с окнами — хендлы окон и отправка сообщений разнообразного состава им через PostMessage — это самые настоящие объекты с самым настоящим механизмом динамической диспетчеризации.
    С другой стороны строка программы вида "some.method(...)" не означает, что some это обязательно объект в смысле ООП. Это может быть обычная структура из процедурного программирования, обмазанная синтаксическим сахаром упрятывания процедуры в имя структуры — пока нет виртуального метода — нет и объекта (в смысле ООП, хотя можно говорить в иных смыслах — "утилитарный объект" и так далее).

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

    В заключение приведу слова пионера ООП — Алана Кэя (создателя Smalltalk):

    "Мне жаль, что давным давно я использовал термин «объект» для этой темы, потому что из–за этого многие люди фокусируются на меньшей из идей. Большая идея — это «сообщения»"
    "ООП для меня это сообщения, локальное удержание и защита, скрытие состояния и позднее связывание всего."
     
    alex_dz, R81... и Intro нравится это.
  2. Intro

    Intro Active Member

    Публикаций:
    0
    Регистрация:
    29 авг 2009
    Сообщения:
    559
    Для меня ООП это - объект и его свойства. Я больше практик и в теориях весьма слаб, образования средние.
    Берём объект и делаем на основе его новый, с дополнительными свойствами. Есть ракета, умеет просто летать по инерции, испытывает сопротивление воздуха с учётом индуктивного, а так же имеет свойство стабилизации.
    А потом по наследованию, делаем ракету с реактивным двигателем, т.е. новый класс. Потом ещё один класс, самонаводящаяся ракета. И так далее.
    Более сложными методами, типа функциональное программирования не владею.
     
  3. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    436
    Главное что тут надо понять, что ООП это, по этой аналогии, когда мы имеем коллекцию игровых сущностей типа collection of GameObject и раздаём им в функциях update и draw сообщения примерно таким образом: foreach gameObject in gameObjects do gameObject.draw() - тогда добавляя новый Rocket или SelfGuidedRocket или LandMine в будущем мы уже не трогаем костяк написанного игрового цикла (не делаем чудовищные switch gameObject.type по всем кишкам программы с необходимостью их правки повсюду при появлении нового type), а просто добавляем новый модуль с новым типом объекта и он бесшовно встраивается в уже существующий код. Вот ради этого главным образом ООП и изобретался и смысл всей этой затеи не в наследовании или инкапсуляции, а исключительно вот в этом вот полиморфизме.
    При этом инкапсуляция это просто здравый смысл и выше я демонстрировал как он работает и без ООП в FILE * или хендлах WinAPI.
    Наследование же очень естественным образом само начинает возникать когда мы будем пытаться реализовать полиморфизм.
    Вот даже взять совершенно перпендикулярный class-ам из C++ или Java механизм реализации GUI в WinAPI - там есть понятие WNDCLASS (ну прямо вот класс!) в экземпляре которого хранится WndProc - краеугольный камень полиморфизма "экземпляров класса" HWND. И да, там можно "сабклассить" эти классы просто в своём WndProc, причём официально есть два варианта как это делать - по старому и по новому: https://learn.microsoft.com/en-us/windows/win32/controls/subclassing-overview В самом деле - разве не логично взять уже готовое окно кнопки, подменить ему WndProc на свою, где поменять поведение WM_PAINT, а все остальные сообщения перенаправить старому обработчику? Более чем логично. А ведь это и есть наследование.
    В общем и инкапсуляции и наследование это просто детали реализации полиморфизма в разных средах и условиях. Так получилось, что когда ООП набирал обороты доминировал он в C++, а немного позднее Java и их способ реализации ООП лёг в основание всех этих мантр про "триединство наследования/инкапсуляции/полиморфизма". Но вот этим и навелась тень на плетень и это породило огромное количество порожняковой воды в книгах и дефинициях.
    --- Сообщение объединено, 14 ноя 2023 ---
    Причём замечу еще одну интересную историческую несправедливость и гнильцу в том как ООП пропагандировалось усилиями многих адептов - очень сильно повлияла выкристаллизованная модель языка Java где вообще все объекты имеют полиморфные (виртуальные) методы и казалось бы идеальный модельный язык для разработки и пропаганды ООП.
    Но в Java очень долго упускался очень важный механизм который у вменяемой реализации должен быть - это ООП-callbacks.
    В результате навертелось очень много мусорных и чуть ли не неправильных "идиом" или "паттернов программирования" когда любую в жопе затычку реализовывали через полиморфизм вовлекая в решение мелких задач наследование, т.к. по другому в ранней Java было нельзя сделать.
    Классический пример - интерфейс IButtonListener. Типа если нужно принимать сообщение о нажатии кнопки, то надо отнаследоваться от IButtonListener чтобы получить в своём классе виртуальный метод onButtonClick, его переопределить и быть довольным.
    Но на самом деле для данной задачи это неудобно и непрактично. Просто в ранних Java по другому было нельзя. ООП здесь должен быть дополнен расширением того, что в модульном программировании называется "указатель на функцию". В ООП соответственно нужен "указатель на метод". То есть просто такой тип который ведёт себя как функция с определенной сигнатурой при этом это метод какого то существующего объекта с такой же сигнатурой и прошитой ссылкой на этот объект, чтобы в момент вызова вызывалось что нужно.
    Те кто программировал в Delphi уже знает что речь про то как там реализуются event-ы. Знакомые с C# опять таки понимают, что речь про делегаты. В общем просто аналог указателя на функцию, только для объектов.
    Тут интересно, что в C++ есть тип тоже называемый "указатель на метод", но парадокс в том, что это не то, что нужно и работает оно калечно и неправильно для данного применения. Долго объяснять. Но к чести C++ обмазав свою реализацию "из коробки" указателей на методы они всё-таки в STL сделали нормальный правильный шаблонный тип - std::function. Вон он, за некоторыми досадными деталями, то, что нужно. В std::function можно положить спряжение ссылки на объект с указателем на его метод так, что внешне будет выглядеть как просто callable объект с нужной сигнатурой из которой как бы вычеркнуто то, что вообще есть этот объект внутри в кишках. И это именно то, что нужно - какой именно объект сидит в делегате и какого он класса нам совершенно неважно - это может быть абсолютно что угодно никак не сцепленное с чем то наследованием и названием метода - нам нужно только чтобы у метода была нужная сигнатура, а объект может быть любым вообще.
    Более того, с десятилетиями и Java пришла к этой формуле, правда тернистым путём, и теперь в ней есть тоже "method pointers" с нужным поведением и семантикой.
     
    Последнее редактирование: 14 ноя 2023
  4. mantissa

    mantissa Мембер Команда форума

    Публикаций:
    0
    Регистрация:
    9 сен 2022
    Сообщения:
    138
    Еще же есть ОО Анализ, который часто отождествляют с крайней необходимостью при использовании ООП, т.е. использование различных паттернов, техник и т.д. то есть программировать в ООП стиле без нормального проектирования - нет смыла, нужно поддерживать модульность, малую связность и т.д. и т.д. Многие следуют заповедям Лармана, GRASP шаблоны, унифицированная разработка, когда с UML диаграмм можно получить скелет на Java
     
  5. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    436
    Опять таки эти все навороченности - это во многом наведение теней на плетень.
    Так как ООП вершина обозначенной в первопосте эволюции программирования, то оно конечно понятно, что когда у теоретико-практиков зачесалось написать многотомники "как правильно писать программы" они не могли обойти вниманием ООП, но превращать сложные программы в одно только сплошное ООП вдоль и поперёк - это наведение теней на плетень.
    ООП нужен ровно там где он нужен - там где хочется, например, с колллекцией разных объектов работать единообразным образом - в редакторе текста с картинками чтобы и параграфы текста и картинки между ними программно имели один интерфейс onClick/onKeyPress/onPaint. Или когда компонентная модель HTML-документа там тоже ворох всего, но у всего есть style, clientRect, эвенты и так далее - тут ООП само просится.
    И оно действительно нужно именно в сложных конструкциях программы исходя даже просто по тому какую задачу решает - именно чтобы один и тот же код работал единообразно с потенциально разными объектами с разным поведением. Это уже не крестики-нолики, тут нужна какая то более-менее сложная система.
    При этом есть реально пример когда крестики-нолики программируются в ООП-стиле: https://habr.com/ru/articles/110247/
    Чувак сделал 4 класса - "солверы по горизонтали, вертикали и горизонталям" чтобы они виртуальными своими методами разруливали четыре проверки :lol: это, конечно, сильно, но лишь показывает, что можно довести до абсурда что угодно.
    Но ООП это не паттерны проектирования, а всего лишь один вот этот вот простой в сущности приём программирования - заложим в объект некий динамический селектор процедур по его обработке так чтобы имея дело с объектами с разными процедурами и данными код не должен был знать с какими именно.
     
  6. alex_dz

    alex_dz Active Member

    Публикаций:
    0
    Регистрация:
    26 июл 2006
    Сообщения:
    315
    aa_dav
    какой по вашему следующий шаг после ООП? или ето вершина еволюции и дальше только Господь?
    --- Сообщение объединено, 14 ноя 2023 ---
    взять для пример ДНК и человека + животный мир
    как тут ООП байндится?
    коим образом тут мутации? вопросов много
    ответов мало
     
  7. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    436
    Так главная мысль этой темы, что ООП это не "целый мир", а один конкретный практический трюк программирования.
    И на ООП всё не остановилось. То же шаблонное программирование в С++ намного более комплексная и сложная техника. Вот сравнимы по величине сложности с ООП пожалуй дженерики в Java/C#. То есть это сколько мозгов надо включить чтобы понять и овладеть.
    Из последних писков моды можно вспомнить как функциональщина полезла изо всех щелей - неплохая штука, но что забавно, что когда на её лямбдах стали делать промисы, то оказалось что это всё-таки не просто не удобно, а вообще неудобно, что в свою очередь породило такой писк моды как асинхронные всякие корутины и эти вот всякие жидкие замены полновесным потокам в виде асинхронного программирования.
    Да мало ли что еще будет...
    Но чисто по злобе дня я думаю писки моды будут всё дальше уходить в трюки такого толка чтобы облегчать массивную многопоточку.
     
  8. alex_dz

    alex_dz Active Member

    Публикаций:
    0
    Регистрация:
    26 июл 2006
    Сообщения:
    315
    OOP (vmt/vfp) vs templates - совсем разые вещи
    лямдбу уже тоже прикрутили в С++, хотя не нравится оно мне, как-то чужеродное тело чтоль

    если я верно понял ваш ответ - следующего (квантового) скачка по аналогии
    модульное-ООП программирование пока не случилось

    да и вряд ли случится .. когда ООП придумали? десятки лет тому назад, с тех пор ничего нового
    учитывая ескпоненциальный рост кол-ва язиков-программистов - всего программируемого как-то печально получается
     
  9. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    436
    Да не было никакого "квантового скачка", было вполне логичное закономерное развитие языков в соответствии с неизбежно как расцвет социализма логикой программотворчества.
    Например шаблонное программирование хотя и не стало повсеместным, но в виде дженериков вполне себе распространилось на прочие языки, заползло в этом аспекте не только в Java/C#, но и в Delphi ибо действительно в ЯВУ со строгой типизацией не иметь параметризуемых типом контейнером - ну это же фу.
    С объектами произошла же во многом пыль в глаза - 50% объектов/классов в программах не являются ООП-нутыми. Это просто вышло так, что в новом синтаксисе всё стало выглядеть как class даже если по своей сути это банальный struct. И главная характеристика настоящего class - что у него собираются использовать наследников через ссылку на базу для эксплуатации виртуальных функций.
    И вот если разобраться по чесноку, то огромная часть классов в типовых программах - они не объекты, а структуры в фантике запрятывания функций принимающих первым параметров ссылку на структуру в неймспейс класса.
    В результате этой тучи поднятой пыли и непонимания что на самом деле такое ООП и сложилось впечатление, что вот не было не было, а потом как вдруг накатило и подавило и заменило собою всё на свете.
    Но нет, не было такого. С таким же успехом можно утверждать, что дженерики накатили и заменили собою всё на свете. Да, заменили, но не всё на свете, а либо нетипизированные контейнеры как TList в Delphi либо их специализации создаваемые вручную для каждого нужного случая. Да, здесь пригодилось.
    А так - да, полиморфизм очень нужная в сложных программах вещь. На втором месте после контейнеров.
     
  10. Intro

    Intro Active Member

    Публикаций:
    0
    Регистрация:
    29 авг 2009
    Сообщения:
    559
    class отличается от struct, просто наличием методов, а виртуальные они или нет, и есть ли наследники, зависит от сложности класса. Если класс простой, то и не надо! По крайней мере я это так понимаю.
     
  11. alex_dz

    alex_dz Active Member

    Публикаций:
    0
    Регистрация:
    26 июл 2006
    Сообщения:
    315
    в структуре тоже можно использовать методы
     
  12. rmn

    rmn Well-Known Member

    Публикаций:
    0
    Регистрация:
    23 ноя 2004
    Сообщения:
    2.327
    Нет. class отличается от struct только тем, какой доступ к членам по умолчанию, если ты явно не укажешь public/protected/private.
     
  13. mantissa

    mantissa Мембер Команда форума

    Публикаций:
    0
    Регистрация:
    9 сен 2022
    Сообщения:
    138
    это в плюсах так, в других языках по другому. тут больше про саму парадигму, чем про конкретную реализацию в каком-то ЯП
     
  14. Intro

    Intro Active Member

    Публикаций:
    0
    Регистрация:
    29 авг 2009
    Сообщения:
    559
    Я знаю что в структ можно вкарячить методы, но это уже класс, значит надо переименовать объект в класс. Я бы, вообще запретил в struct добавлять методы.
     
    mantissa нравится это.
  15. rmn

    rmn Well-Known Member

    Публикаций:
    0
    Регистрация:
    23 ноя 2004
    Сообщения:
    2.327
    Да кому нужны эти другие языки? Весь серьезный софт пишут на C/C++, остальное - хипсторам поиграться и просмотров на ютубчике насобирать :)
     
  16. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    436
    Если вкратце, то код такого толка:
    Код (C++):
    1.  
    2. class MyClass
    3. {
    4.   void meth(int x);
    5. };
    6. ...
    7. MyClass x;
    8. x.meth(10);
    9.  
    является синтаксической обмазкой сахаром следующего полностью эквивалентного "модульно-структурного" кода:
    Код (C++):
    1.  
    2. struct MyClass
    3. {
    4. };
    5. void MyClass_meth(MyClass *this, int x);
    6. ...
    7. MyClass x;
    8. MyClass_meth(&x, 10);
    9.  
    с точностью до машинного кода.
    Любой класс без виртуальных методов - это старое доброе модульно-структурное программирование из 70-х ровно как FILE*/fopen.
    Только когда добавляешь виртуальные методы и начинаешь ими пользоваться возникает объектно-ориентированный дизайн в программе.
     
    mantissa нравится это.
  17. Intro

    Intro Active Member

    Публикаций:
    0
    Регистрация:
    29 авг 2009
    Сообщения:
    559
    aa_dav, модульно-структурное. Это чтобы элементарно избавиться конфликта имён, а то раньше все функции были глобальными и имена конфликтовали. А ещё классы, простые классы без всяких виртуальных методов, лучше оптимизируются, т.е. получается С++ создаёт чуть более оптимизированный код, компилятор может между своих методов, this определить в защищённый регистр(esi,edi,ebx), а не как обычно ecx, что несколько быстрей, правда совсем ненамного, но быстрей. Так что С++ это более быстрый ЯП чем простой С, но только если кодить в определённом стиле.
     
  18. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    436
    Это не такой простой вопрос из-за RVO, но суть, это перпендикулярная мысль.
    Но так или иначе ООП это один конкретный приём программирования когда мы структуру данных наделяем полиформизмом как то запрятывая в неё возможность вызывать заранее неизвестные участки кода.
    Интересно посмотреть как на ООП можно программировать на Си.
    Мне очень нравится эпохальные программы из 90-х годов чьи исходные коды раскрыты - это Quake 1/2 за авторством Джона Кармака.
    Quake 1 написан почти полностью в модульном стиле. Вот пример классического до-ООП-ного модуля из его недр: https://github.com/id-Software/Quake/blob/master/WinQuake/cvar.h
    Cvar-ы это глобальные настройки игры Quake которые можно изменять в консоли игры.
    И вот тут мы видим прекрасно классический модуль из структурного программирования - определена структура cvar_t и несколько функций которые либо принимают указатель на неё в качестве аргумента либо возвращают его как результат:
    Код (C++):
    1.  
    2. cvar_t *Cvar_FindVar (char *var_name);
    3.  
    Ну и плюс всякая обвязка для удобства задания или чтения cvar по текстовому имени.
    Сейчас интересно заглянуть в модуль вывода и реакции на нажатия меню: https://github.com/id-Software/Quake/blob/master/WinQuake/menu.h
    Просто несколько функций торчат из модуля наружу которые всё разруливают и если заглянуть в реализацию https://github.com/id-Software/Quake/blob/master/WinQuake/menu.c , то это просто куча функций которые жёстко фиксируют какие меню и их пункты есть - всё захардкожено и просто есть два глобальных указателя на функции - какая функция из этого вороха сейчас рисует меню, а какая - обрабатывает реакцию на нажатия кнопок от пользователя.

    Но посмотрим на модуль меню у Quake 2: https://github.com/id-Software/Quake-2/blob/master/client/qmenu.h
    Код (C++):
    1.  
    2. typedef struct
    3. {
    4.    int type;
    5.    const char *name;
    6.    int x, y;
    7.    menuframework_s *parent;
    8.    int cursor_offset;
    9.    int   localdata[4];
    10.    unsigned flags;
    11.  
    12.    const char *statusbar;
    13.  
    14.    void (*callback)( void *self );
    15.    void (*statusbarfunc)( void *self );
    16.    void (*ownerdraw)( void *self );
    17.    void (*cursordraw)( void *self );
    18. } menucommon_s;
    19.  
    Вот оно - на чистом Си Кармак в Quake 2 устал плодить кучу сильно связанных функций в модуле Menu и рисовать закат солнца руками и начать делать... ООП - он положил в структуру menucommon_s указатели на функции которые первым параметром принимают указатель на себя же (self) и по сути изобрёл виртуальные функции. Но не зря эта структура называется menucommon_s - common это потому что чуть ниже он переизобретает еще и наследование. Вот чтобы наследование лучше легло в реалии языка Си у него self есть void *, иначе везде пришлось бы кастить из одного в другое.

    Вот так Джон Кармак в 90-ые медленно но верно шёл к объектно-ориентированному миру программирования и только Doom 3 (!) он написал уже пересев полностью на C++ и пользуясь ключевым словом class налево и направо.
    Старовер был, но всё-равно прогресс его доконал. :lol:

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