Несмотря на то, что сам я ушел из большого ООП¹ более десяти лет назад, причем, надеюсь, навсегда, я всегда крайне вяло и неохотно участвую в баталиях тупоконечников и остроконечников: я абсолютно убежден, что для разных типов задач лучше подходят разные инструменты, и выхолощенное ФП заставит всех вокруг создавать тонны никому не нужного бойлерплейта для тривиального круда, а кристальное ООП — воткнет все возможные палки в колёса при реализации бизнес-процессов. Любой из современных языков программирования позволяет смешивать эти подходы, а микросервисная архитектура — даже гостеприимно приютит несколько языков и сред под одной крышей.
Тем не менее, хотя я никогда не считал себя евангелистом функционального подхода, и уж, тем более, не примыкал к стану воинствующих пуристов, меня постоянно свербил вопрос: что же все-таки не так с ООП, если лично мне быстрее, проще и понятнее — реализовывать свои проекты на функциональном эликсире?
И вот, наконец, меня озарило. Объектная модель всем хороша в однопоточной среде. Даже банальная асинхронность приносит кучу совершенно нерелевантных проблем: мьютексы любого сорта — это порождение дьявола. В игрушечных примерах из книжек они еще как-то работают, но действительно многопоточный код на них написать фактически нереально. Среда, которая буквально приглашает разработчика ошибиться и разрушить тотальность функций потенциальным дедлоком — не должна иметь права на существование в принципе.
Пользуясь случаем, расскажу байку про дедлок из реальной жизни, которую я всегда рассказываю студентам, для которых само это понятие — внове. Я считаю этот анекдот — лучшим объяснением сущности того, что называется дедлок.
В аэропорту города N из-за погодных условий несколько часов не принимали рейсы. Некоторые самолеты ушли на соседние аэродромы, но некоторые были задержаны в портах вылета. Городок N — маленький, аэропорт тоже, обслуживается одним диспетчером. Ровно в час X у него закончилась смена, и он ушел домой. Сменщик должен был заступить на вахту спустя десять минут, чтобы посадить подлетающий еще через 20 минут самолет, из числа задержанных.
Это чуть не привело к катастрофе, потому что диспетчер следующей смены возвращался из отпуска. На задержанном самолёте. Посадить который было, понятно, некому.
Эта ситуация: самолёт ждёт указаний диспетчера, но диспетчер не сможет дать никаких указаний, пока самолёт не сядет, — и есть типичный дедлок.
Итак, мьютексы — это несомненное зло (любой, кто раскрыл рот, чтобы сообщить мне, что я просто не умею их готовить — приглашается к соревнованию на написание правильно синхронизированного кода, в который вовлечены 16 потоков). Лучшее решение, которого сможет добиться программист высокого уровня, — это вернуться на мьютексах в однопоточную модель вычислений. По сути, джавовский synchronize
делает именно это.
Объект, представленный в памяти статическим набором данных и сопутствующего кода, — с огромным трудом выживает в многопоточной среде. Он к этому просто не приспособлен. Инкапсуляция — в том виде, в котором она реализована в классических ООП языках — превращается в тыкву. Смотрите:
public int increment() {
int current = getDbValue();
// управление переключилось
setDbValue(++current);
return current;
}
Два вызова метода increment
из двух разных потоков приведет к тому, что мы дважды вычитаем предыдущее значение из базы, а потом дважды запишем обратно его же, увеличенное на единицу. Классический off-by-one в условиях высококонкурентной среды. (Для высокоинтеллектуальных комментаторов, умеющих в атомарные инкременты в своих любимых базах данных: это пример, база тут вообще ни при чем, замените базу на отсылку имейла или типа того, если вам так понятнее.)
Иными словами, столь тщательно выстроенные годами паттерны, абстракции и вообще краеугольные камни ООП — рушатся при распределении нагрузки по всем процессорам. Инкапсуляции из коробки больше нет.
С наследованием всё не так ужасно, но тоже не идеально: если в кластере одна машина перезапустилась в процессе раскатки обновлений, а вторая — пока нет, виртуальные методы будут разрешаться в инстансы разных классов (редкие грабли, да, но во-первых, я на них наступал, а во-вторых — это задача языка защитить разработчика от такого фокуса).
Среди шаблонов параллельного программирования, согласно Вике, более половины — разнообразные блокировки и остановки, но самое прекрасное — наличие там паттерна «Однопоточное выполнение». Остальные шаблоны, заботливо выдуманные за годы эмпирического анализа проблем, тоже ломаются буквально через один: Singleton, Factory (если нужен учёт создаваемых объектов), Composite, Strategy, Observer…
Иными словами, в ООП нет ничего прям плохого, но только пока вы не погрузились в сильно связанный хайлоад. Вот тогда придется написать заново неспецифицированную, глючную и медленную реализацию половины акторной модели². ООП был современной и крайне удобной парадигмой в девяностые, когда многопоточное связанное программирование было уделом фриков. В 2025 эта парадигма всячески сопротивляется распараллеливанию задач, что при современной доступности количества ядер на единицу выполнения кода — халатность, граничащая с преступлением.
Чтобы победить родовые травмы объектных языков программирования, инкапсуляцию придётся принести в жертву иммутабельности, а наследование — полиморфизму и инъекции зависимостей. Иными словами, придётся писать практически в функциональной парадигме, попутно сражаясь с примитивами языков, принципиально заточенных в бо́льшей степени на объектную модель.
Такие дела, и удачной объективизации!
¹ Для занудных буквогрызов: я использую термин ООП не в первородном смысле, в котором его первым употребил Алан Кай, а в том, которое повсеместно распространилось сейчас с легкой руки Гослинга — наследование, инкапсуляция, классы. Простите.
² Отсылка к десятому правилу Гринспена.
Комментарии (343)
rsashka
23.06.2025 09:40любой, кто раскрыл рот, чтобы сообщить мне, что я просто не умею их готовить ...
Два вызова метода increment из двух разных потоков приведет к тому, что мы дважды вычитаем предыдущее значение из базы, а потом дважды запишем обратно его же, увеличенное на единицу.
Оказывается, у вас проблемы не только с мьютексами, но и с БД
cupraer Автор
23.06.2025 09:40Да я вообще тупой, что уж там.
rsashka
23.06.2025 09:40Так не нужно корявым примером работы с БД аргументировать наличие проблем в ООП. Или используйте атомарный инкремент средствами самой БД или меняйте алгоритм работы класса на многопоточный.
Ведь ни мьютексы, ни ООП тут не причем, так как эти инструменты действительно нужно уметь правильно
готовитьиспользовать.Dhwtj
23.06.2025 09:40атомарный инкремент средствами самой БД
ООП как бы обещает защиту данных через инкапсуляцию и абстракцию, а у вас абстракция протекает
rsashka
23.06.2025 09:40Вы можете развернуть свой комментарий? Если честно, то я его не понял.
cupraer Автор
23.06.2025 09:40Чтобы инкапсуляция работала, как задумано, — надо правильно написать код внутри.
Dhwtj
23.06.2025 09:40фальшивые дублоны принимаете?по быстрому могу только через LLM
Обещание ООП
ООП говорит: "Вот объект
BankAccount
. У него есть приватное поле_balance
. Только его методы, напримерDeposit()
, могут менять это поле. Я, объект, гарантирую целостность своих данных". Это и есть инкапсуляция. А абстракция в ООП говорит, что мы не должны заботиться о реализации - БД это или скажем in-memory key-value словарь.Суровая реальность (где протекает абстракция)
На самом деле, "истинное" значение баланса лежит не в памяти моего C# объекта, а в строке таблицы
Accounts
в PostgreSQL. Мой объект — это всего лишь временный, кешированный представитель этих данных.И тут возникает конфликт:
-
Наивный ООП-подход (протекающая абстракция):
Мой метод
Deposit(amount)
делает:this._balance += amount;
Затем другой слой (Repository/ORM) смотрит на измененный объект и генерирует SQL:
UPDATE Accounts SET Balance = [новое значение] WHERE Id = ?
Проблема (Race Condition): Если два потока одновременно загрузят счёта с балансом 1000, оба в памяти увеличат его до 1100, и последний записавший затрет результат первого. Баланс будет 1100, а не 1200. Инкапсуляция объекта в C# никак не защитила данные в БД. Абстракция "безопасного объекта" протекла и оказалась ложью.
-
Правильный подход (признание, что ты не главный) - протекающая абстракция:
Как вы и сказали, нужно использовать
UPDATE Accounts SET Balance = Balance + 100 WHERE Id = ?
.Эту операцию БД выполнит атомарно. Она сама защитит свои данные.
Что это значит для моего ООП-объекта? Его метод
Deposit()
больше не должен менять свое состояниеthis._balance
. Он должен сгенерировать команду (UPDATE...
) и отправить ее в базу данных.
Итог:
Когда появляется внешний авторитетный источник состояния (как БД), классическая ООП-инкапсуляция и абстрагирование от реализации в БД ломается. Объект больше не является хозяином своих данных. Он превращается из хранителя состояния в генератор команд (Command Emitter) или фасад для внешних операций.
Попытка притвориться, что объект в памяти — это и есть "настоящий" объект, и есть та самая "протекающая абстракция". Правильное проектирование (например, в CQRS) признает этот факт и явно разделяет модели для чтения и модели для изменения, которые только принимают команды и валидируют их, а всю работу по атомарному изменению делегируют БД.
cupraer Автор
23.06.2025 09:40Фу. Это мой пример, только мысью по древу.
Если нужен правильный пример без стороннего источника данных, то лучше взять стейт-машину, с «инкапсулированным» стейтом, который точно так же течет без всякой базы, и вылечить это без мьютекса уже невозможно.
Dhwtj
23.06.2025 09:40ну какой смог, я просто расписал как обещанная абстракция протекает
просто в терминах statemachine не все мыслят
qrKot
23.06.2025 09:40У вас странное понятие о протекающих абстракциях.
ООП говорит: "Вот объект
BankAccount
. У него есть приватное поле_balance
. Только его методы, напримерDeposit()
, могут менять это поле. Я, объект, гарантирую целостность своих данных". Это и есть инкапсуляция.Это не инкапсуляция.
"Я объект BankAccount, представляющий банковский счет. У меня есть методы Deposit и Withdraw, позволяющие менять баланс счета. Я гарантирую целостность своих данных. Как я это делаю и в каком виде храню баланс - не твое собачье дело" - вот это инкапсуляция.
На самом деле, "истинное" значение баланса лежит не в памяти моего C# объекта, а в строке таблицы
Accounts
в PostgreSQLВообще по барабану, где оно лежит и как реализовано до тех пор, пока подробности не торчат наружу.
Смысл абстракции именно в том, чтобы ты не знал, что внутри. Это гарантия, что ты можешь безопасно поменять внутреннюю реализацию, не задевая зависящих от нее частей программы.
Вот у тебя вышеуказанный BankAccount. Допустим, ты пишешь реализацию: методы Deposit и Withdraw набирают номер телефона оператора и заставляют нейросеть зачитать голосом "увеличить баланс счета №192839102931012 на 100 рублей". Реализация говно, конечно, но абстракция - идеальна.
Почему идеальна абстракция? Ну, например, ты достаточно быстро понял, что дозвон может занимать до 5 минут, что сильно тормозит поток транзакций. Надо что-то делать. Например, батчевать. Пока номер набирается, копить команды и вываливать на оператора разом то, что накопилось: "счет 1923891243 +100, счет 932181292348 -100". Прелесть в том, что весь твой код, который подсчитывает, на сколько менять баланс, вообще ни в одной строчке не изменился. Все изменения - внутри класса BankAccount.
Окей, на 18-й итерации до тебя дошло, что с оператором что-то не так: то опечатывается, то трубку не берет, то рыдает и в запой уходит. Ты ставишь какую-нибудь постгрю и выкидываешь к чертовой матери всю эту дичь с телефонией. Заменяешь тупым запросом к БД. Изменения - монументальные, а абстракция не изменилась. Чтобы поменять баланс, тебе нужно дергать все те же ручки с теми же параметрами.
Вот это - абстракция, и она не течет. Хочешь пример текущей абстракции - посмотри на слайсы в Go. А вот то, что ты напридумывал, не течет.
cupraer Автор
23.06.2025 09:40И тут мы вспоминаем, что данное обсуждение происходит в комментариях под определенным текстом, добавляем второй поток обработки (это — по сути тема текста) — и ваша прекрасная абстракция начинает не просто подтекать, а фонтанировать.
Иногда полезно немного думать, прежде чем что-то писать в публичный доступ.
qrKot
23.06.2025 09:40Иногда полезно немного думать, прежде чем что-то писать в публичный доступ.
Вот это я вам и пытаюсь донести.
И тут мы вспоминаем, что данное обсуждение происходит в комментариях под определенным текстом, добавляем второй поток обработки (это — по сути тема текста) — и ваша прекрасная абстракция начинает не просто подтекать, а фонтанировать.
Каким боком она подтекать-то начинает???
Вот у нас метод
BankAccount.Deposit(decimal amount)
- это наша абстракция.public void Deposit(decimal amount) { this.amount += amount }
Да, допустим, мы тупые и не предусмотрели возможность конкурентного доступа. И что? Абстракция-то тут причем, проблема-то в реализации.
Вот, смотрите, ща я реализацию исправлю, а абстракция та же самая останется:
public void Deposit(decimal amount) { this.mutex.Lock() this.amount += amount this.mutex.Unlock() }
А абстракция та же, ничего никуда не утекло!
Так что, прежде чем писать в публичный доступ о текущих абстракциях, потрудитесь, хотя бы, нагуглить, что такое "текущая абстракция"
Dhwtj
23.06.2025 09:40В многопотоке абстракция течёт. Потому что появляется время, которое разделяет состояние на до изменения и после. Но если объект сам не управляет временем, то до и после перемешивается.
Приведите пример в многопотоке.
Lock управляет временем (заставляет ждать), но способ не безопасный и не эффективный
qrKot
23.06.2025 09:40В многопотоке абстракция течёт. Потому что появляется время, которое разделяет состояние на до изменения и после.
Ну, т.е. любая абстракция течет? о_О Всегда есть время до и после!
Приведите пример в многопотоке.
Приводил уже.
public void Deposit(decimal amount) { this.mutex.Lock() this.amount += amount this.mutex.Unlock() }
Lock управляет временем (заставляет ждать)
Все верно, в этом и есть смысл абстракции! Абстракция - это класс BankAccount, который позволяет просто дернуть ручку Deposit, на задумываясь, что внутри. Мьютекс - внутри, в реализации. Нам не надо заботиться о необходимости лочить мьютекс снаружи, потому что внутри мы этим уже озаботились - это и называется "абстракция".
но способ не безопасный и не эффективный
"Других у меня для вас нет". Но, точнее, есть, но они все так или иначе либо небезопасные, либо неэффективные.
-
Dhwtj
23.06.2025 09:40Вы можете развернуть свой комментарий? Если честно, то я его не понял
Могу и развернуть:
Учите, что такое инкапсуляция и абстракция. А потом задавайте вопросы.
rsashka
23.06.2025 09:40Учите, что такое инкапсуляция и абстракция. А потом задавайте вопросы.
Вам самим это не мешает хорошенько выучить, так как LLM за вас думать не будет.
А с точки зрения ООП, речь всегда идет о внутренних данных объекта, тогда как в примере в статье используются внешние по отношению к объекту данные из БД.
cupraer Автор
23.06.2025 09:40с точки зрения ООП, речь всегда идет о внутренних данных объекта, тогда как в примере в статье используются внешние по отношению к объекту данные из БД
public int increment() { int current = this.value; // управление переключилось this.value = ++current; return this.value; }
Завидую, когда у людей абстрактное мышление полностью вытеснено прикладным. Так понятнее?
rsashka
23.06.2025 09:40Конечно! Ведь в этом примере можно вызывать increment() хоть в миллиарде потоках без каких либо гонок!
cupraer Автор
23.06.2025 09:40Вы сейчас серьёзно? Вы правда думаете, что гонки тут нет?
nin-jin
23.06.2025 09:40Конечно нет. В начеле метода компилятор вставит захват мьютекса, а в конце его освобожнение.
cupraer Автор
23.06.2025 09:40В начале метода компилятор вставит захват мьютекса, а в конце его освобождение.
Так и пишите: если класс напрямую объявлен, как
synchronized
, то […]. Так и джава умеет, вот только это кувалда, которая решает одну незатейливую проблему, а приносит — миллиард очень затейливых, наподобие неожиданных дедлоков в самых непредсказуемых местах.Методы экземпляров нормальных объектов, про которые в этом треде идет речь, ни в какие мьютексы, разумеется, не оборачиваются, и гонка в примере выше есть.
nin-jin
23.06.2025 09:40А не синхронизированный класс компилятор не даст вам расшарить между потоками.
cupraer Автор
23.06.2025 09:40Чаво? [Здесь и далее я предполагаю, что под «не синхронизированный класс» имелось в виду «экземпляр класса, объявленного без ключевого слова
synchronized
».]А как компилятор узнает, что я собираюсь экземпляр передать в другой поток?
cupraer Автор
23.06.2025 09:40Понятнее не стало, а тратить время на подробное разбирательство с D у меня желания нет.
Насколько я понял, чтобы передать экземпляр в другой поток, я должен руками объявить его как
shared
. Что, конечно, прям повышает удобство вывода в параллельные вычисления любого старого кода. Borrow checker в расте делает примерно то же самое, но язык от этого не становится волшебным образом готов к параллелизму.
Dhwtj
23.06.2025 09:40Причём тут язык D? Мы более абстрактно, не зависимо от языка обсуждаем вроде
Или по вашему ООП только там? Java не ООП?
Dhwtj
23.06.2025 09:40Значит, ООП сам по себе не даёт гарантий многопоточности. И инструментов удобных в общем случае нет. О чём и спич
cupraer Автор
23.06.2025 09:40Гарантии безопасного параллелизма даёт система типов?
Глаза же вытекают, не надо так.
rsashka
23.06.2025 09:40Если вы вызываете метод increment() у одного объекта из нескольких потоков (когда объект глобальный), тогда естественно гонка будет. Если объект локальный для потока (сервис, асинхронщина и т.д.), то гонки нет.
Тогда как в вашем изначальном примере с внешними данными из БД гонка есть всегда и не зависимо от локальности объекта.
cupraer Автор
23.06.2025 09:40в этом примере можно вызывать increment() хоть в миллиарде потоках без каких либо гонок!
Если вы вызываете метод increment() у одного объекта из нескольких потоков (
когда объект глобальный), тогда естественно гонка будет.Я вычеркнул бессмысленную часть комментария, потому что объекту вообще необязательно быть глобальным, это может быть любой объект.
Так вот, вопросик: какое из ваших альтер эго получает право решающего голоса? С каким из этих взаимоисключающих утверждений вы согласны прямо сейчас, в эту секунду?
rsashka
23.06.2025 09:40это может быть любой объект.
Вариантов объектов может быть целая куча, плюс еще маленькая тележка.
Только в С++ время жизни объекта может быть глобальным, локальным для области видимости и локальным для потока.И для каждого варианта стратегия синхронизации доступа к методам объекта будет разной. Точнее, синхронизировать доступ к такому объекту нужно только тогда, когда он глобальный. Для локальных объектов синхронизация не требуется, а вот thread_safe зависит от условий его использования.
Короче, срочно в школу учить матчасть!
cupraer Автор
23.06.2025 09:40Для локальных объектов синхронизация не требуется […]
Это еще почему? Я не имею права внутри функции объявить переменную, присвоить и передать в два разных потока? Компилятор запретит?
rsashka
23.06.2025 09:40Можно и адрес локальной переменой возвращать в return, можно даже писать по 0x0 адресу, никто вам этого не запретит и даже в Rust можно писать unsafe код.
Однако мы ведем разговор про синхронизацию доступа к объекту и в контексте нашей переписки "локальный" объект подразумевает отсутствие доступа к нему снаружи локальной области видимости.
Но для С++ это будет только соглашением, т.к. компилятор это не может проверить.
cupraer Автор
23.06.2025 09:40мы ведем разговор про синхронизацию доступа к объекту и в контексте нашей переписки «локальный» объект подразумевает отсутствие доступа к нему снаружи локальной области видимости
Если честно, то я понятия не имею, о чем ведете разговор вы. Я вяло реагирую на логические ошибки и нестыковки, просто потому, что плохо переношу алогизмы.
Если нет доступа — бессмысленно рассматривать гипотетическую ситуацию «вы вызываете метод
increment()
у одного объекта из нескольких потоков […]» (это прямая цитата из вашего комментария, если что). Если доступ есть — то есть и гонка (пока нет явного мьютекса, или его неявного аналога, известного в джаве под именем «synchronize
»).Всё вот именно так просто, тут нечего обсуждать.
rsashka
23.06.2025 09:40Ну раз нечего обсуждать, тогда удачи вам хорошо пообщаться с LLM в другой ветке комментариев.
Dhwtj
23.06.2025 09:40Видимо, под глобальностью вы имеете в виду обратное от локальности в одной функции. То есть объект могут
иметь с двух сторонТо есть shared state.
rsashka
23.06.2025 09:40Да, что-то вроде этого. Просто классифицировать объекты можно по разному и некоторые названия могут совпадать, несмотря на изначально разные трактовки и вкладываемый в них смысл.
Dhwtj
23.06.2025 09:40с точки зрения ООП, речь всегда идет о внутренних данных объекта, тогда как в примере в статье используются внешние по отношению к объекту данные из БД
Так и есть. Но ООП заявляет (через инкапсуляцию и абстракцию) что оно может решить этот вопрос без погружения программиста в кишки СУБД. И ведь многие верят!
cupraer Автор
23.06.2025 09:40Я уже переписал пример без СУБД (и даже в сам текст дисклеймер вставил). Вот: https://habr.com/ru/articles/920898/comments/#comment_28475464
qrKot
23.06.2025 09:40Это в каком месте ООП такое заявляет? Может, вы просто неверно поняли то, какие гарантии ООП дает?
rsashka
23.06.2025 09:40Эта ситуация: самолёт ждёт указаний диспетчера, но диспетчер не сможет дать никаких указаний, пока самолёт не сядет, — и есть типичный дедлок.
Кстати, насчет мьютексов и дидлоков. Эта проблема не мьютексов, как объектов, а их архитектуры (или интерфейса использования). Два взаимосвязанных метода (захват и освобождение) получаются не связаны между собой в коде программы.
А вот если бы мьютекс был реализован как объект ООП, тогда такой ситуации возникнуть просто бы не могло, так как сам компилятор стал бы автоматически управлять взаимозависимыми вызовами одного и того же объекта (например за счет применения какого нибудь std::lock_guard).
cupraer Автор
23.06.2025 09:40В каком смысле бы не могло возникнуть? Какая разница, как реализован мьютекс, если два несвязанных куска кода в разных потоках могут ждать друг друга? Как это вообще может разрулить компилятор? Это же типичная гонка.
Кроме того, в руби это объект. Да и в джаве это почти объект.
rsashka
23.06.2025 09:40Захват и освобождение мьютекса это всегда два несвязанных между собой куска кода и причем не важно, в одном потоке это происходит или в разных.
Использование концепции ООП позволяет (в теории) обращаться к объекту как к одному целому и контролировать логику его использования, например за счет отказа от вызова отдельных методов (lock и unlock) в пользу применения объектов владения и в этом случае правильностью вызова блокирующих и разблокирующих методов будет управлять компилятор (например, вот так).
cupraer Автор
23.06.2025 09:40Гарантировать отсутствие циклических ссылок можно только путем их запрета на уровне типов (определений классов).
Поскольку это в принципе абсолютно неверно, дальше я читать не стал, прошу прощения.
cupraer Автор
23.06.2025 09:40в этом случае правильностью вызова блокирующих и разблокирующих методов будет управлять компилятор
А, я понял, наконец: вы предлагаете решить проблему гонок запретом на параллельное выполнение кода, оперирующего одними сущностями, то есть, вернуться в однопоток. Перхоть усекновением головы, иными словами. Нет, спасибо.
panzerfaust
23.06.2025 09:40Сниппет с кодом могли бы и получше придумать, если уж так хочется ООП прополоскать. Потому что ООП там не видно. Видно, как одна функция вызывает другие. И видимо под капотом идет общение с базой. Насколько я понимаю, антиппаттерн read-modify-write может стрельнуть и в ООП и в модели акторов. Один актор сам с собой не будет конкурировать, но 2 актора на разных потоках вполне могут.
cupraer Автор
23.06.2025 09:40У меня и в мыслях не было полоскать ООП. Этот пример показывает, почему инкапсуляция методами не такая уж и инкапсуляция. Ну замените там вызовы БД записью в лог, ничего же не изменится.
2 актора на разных потоках вполне могут [конкурировать]
Никто в здравом уме не станет создавать два актора на одну сущность.
panzerfaust
23.06.2025 09:40Этот пример показывает, почему инкапсуляция методами не такая уж и инкапсуляция. Ну замените там вызовы БД записью в лог, ничего же не изменится.
Есть разница между шарингом состояния самого объекта, который действительно защищен только честным словом. И шарингом состояния внешней системы: БД, лога и т.п. Это уже вне рамок языка, фреймворка или парадигмы.
Никто в здравом уме не станет создавать два актора на одну сущность.
Ни один истинный шотландец...
cupraer Автор
23.06.2025 09:40[…] нагадить в БД можно в любых условиях. Был бы доступ.
Ну вот в решении,
под которым мы ведем обсуждение, ставшем темой моей предыдущей заметки, у прикладного (пользовательского) кода — доступа на запись в базу нет. А состояние самого объекта, которое попадает в базу когда надо библиотеке, — защищено на 102% от постороннего вмешательства, вы даже хаками не сможете его изменить.
nuclight
23.06.2025 09:40Почему бы? Акторы имеют только локальные данные и посылку сообщений наружу, так что не могут.
Arlekcangp
23.06.2025 09:40Только локальные данные? А баланс из примера кто хранит? БД? А БД на чем написана? Ну если ООП язык то понятно, "фу, гамно... " и т д и т п. А если нет, и это тоже актор, то тогда как без мьютексов, семафоров, либо других примитивов будут синхронизироваться поступающие данные?
Предвосхищая возможный ответ "этого там не нужно, в эрланге всё асинхронно" e.t.c, допишу дополнение: Если что, все эти эрланги работают поверх ОС, которая "внезапно" использует очереди запросов и "внезапно" снова на мьютексах, критических секциях и вот этом вот всём, так не любимом функциональщиками. Надо признать, что обе самых больших ОС написаны на Си, который не разу не ООП. Но написаны в ООП стиле (особенно windows), где пользовательскому приложению предоставляется АПИ для создания и манипулирования, о Боже!, указателями на инкапсулированное состояние, т е по сути объекты! (Без наследования. Да, больше похоже на объекты в стиле Алана Кеч, но претензии автора поста и особенного его пример с таким же успехом и на такие объекты распространяются. Он же не конкретизировал...)
nin-jin
23.06.2025 09:40ООП и акторная модель многозадачности - вещи ортогональные. Ничто не мешает вам исполнять методы одного и того же объекта на одном и том же потоке, даже если вызывается он из другого.
cupraer Автор
23.06.2025 09:40Разумеется. Но и вот прям совсем никто не помогает, правда? Несмотря на заявленную инкапсуляцию и прочие плюшки.
Dhwtj
23.06.2025 09:40Сделать объект однопоточным? Гениально!
nin-jin
23.06.2025 09:40Объект эксклюзивно владеет своей памятью и не допускает конкурентный доступ к ней, да.
DarthVictor
23.06.2025 09:40То есть в ООП-шном подходе вы в лучшем случае получите ошибку компиляции, как в Rust. Собственно ваша критика сказанное в статье не отрицает, а подтверждает: "Классическое джаваподобное ООП не поддерживает многопоточность."
Вполне логично, потому что полагается на мутабельные структуры. А если эти самые мутабельные структуры из него выкинуть, то окажется, что и методы доступа не очень нужны. И получится ФП.
nin-jin
23.06.2025 09:40Ошибку компиляции при попытке пошарить беззащитную область памяти между потоками. И никакой ошибки в остальных случаях. Например, синхронзированный объект инкапсулирует внутри себя мьютекс, избавляя программиста от необходимости вручную следить за эксклюзивностью доступа к его памяти.
Dhwtj
23.06.2025 09:40синхронзированный объект
Это тоже инструмент, не голый ООП
Причём, есть такое только в Java как я понял.
nuclight
23.06.2025 09:40Это будет совсем не акторная модель уже. Тем более смешно тем, что изначальное ООП как раз и было тем, что сейчас называют акторной моделью.
Dhwtj
23.06.2025 09:40Объект (идентичность) должен защищать свои данные. В частности, в императивном стиле объект получает команду измениться, но он ещё не изменился. И до завершения изменения он получает ещё другую команду измениться.
Тут либо считать его всё ещё той же идентичностью, с однопоточной очередью либо считать до и после разными сущностями.
1. Однопоточная очередь, классика ООП (Мьютекс): Плохо масштабируется. Становится узким местом.
2. Разные сущности (Иммутабельность): Отлично масштабируется для чтения, хорошо для записи.
3. Хитрые блокировки (Оптимистические блокировки): Лучший выбор для смешанных нагрузок, но деградирует при частых конфликтах
Оптимистическая блокировка лучше всего реализуется в ФП в STM. Она сильно зависит от деталей внутренней реализации, т.е. абстракция протекает и в общем случае сложность не оправдана.
VBDUnit
23.06.2025 09:40Есть ещё вариант сразу писать объект на атомарных операциях так, чтобы он спокойно переносил многопоточное использование. Это не всегда возможно, и, зачастую, сложно, но проблему тоже решает.
Dhwtj
23.06.2025 09:40Исключительно lock free, без мьютексов нельзя сделать некоторые задачи.
Перевод денег с одного счета на другой к ним относится. Либо мьютексы и их производные либо eventual consistency. Может, ещё STM
VBDUnit
23.06.2025 09:40Исключительно lock free, без мьютексов нельзя сделать некоторые задачи
Само собой. Но там, где требуется повышенная эффективность, лучше делать lock free (при условии, что это не повысит вероятность багов — а в этих штуках они допускаются легко и потом сложно отлавливаются)
cupraer Автор
23.06.2025 09:40Кажется, в обсуждении появился профессионал-практик :)
А то тут пока только теоретики выступали.
Dhwtj
23.06.2025 09:40Проблема ООП в многопотоке не в таких простых примерах как счетчик.
Правда, абстракция ООП протекла, но что мешает сделать мьютекс руками?
Но сложность и риски реализации эффективных блокировок быстро растет со сложностью объекта.
Пример могу дать, но из LLM, pardon
cupraer Автор
23.06.2025 09:40Да что вы все к примеру-то прицепились? Ну да, пример тривиальный, но суть-то раскрывает ведь?
Конечно, в жизни ошибка будет заковыристее.
Давайте свой пример, чего уж там.
rsashka
23.06.2025 09:40Проблема не в примере, а в выводах, которые вы делаете на основании этого примера.
cupraer Автор
23.06.2025 09:40Да ну? Я делаю вывод: заявленная инкапсуляция — ложь. Демонстрирую это примером. В соседней ветке вам то же самое демонстрируют более навороченным примером с комментариями от LLM. Но вы не понимаете, о чем вам говорят, и продолжаете упорствовать.
Ну, бывает, мне-то что. Я не ставил перед собой цель сделать умнее каждого джуна. Я пишу для тех, кто хочет понять и подумать.
qrKot
23.06.2025 09:40Я делаю вывод: заявленная инкапсуляция — ложь.
Очень странный вывод. Инкапсуляция - это сокрытие, и оно вот прям в вашем примере есть, и даже работает.
nin-jin
23.06.2025 09:40qrKot
23.06.2025 09:40Ну началось. Окей, давайте поанализируем то, что в Вике написано:
Термин «инкапсуляция» может означать следующее в разных языках программирования:
механизм языка, ограничивающий доступ одних компонентов программы к другим;
языковая конструкция, связывающая данные с методами для их обработки.
Слово «инкапсуляция» происходит от латинского in capsula — «размещение в оболочке». Таким образом, инкапсуляцию можно интуитивно понимать как изоляцию, закрытие чего-либо инородного с целью исключения влияния на окружающее, обеспечение доступности главного, выделение основного содержания путём помещения всего мешающего, второстепенного в некую условную капсулу (чёрный ящик).
Итак, мы видим "черный ящик", а также 2 положения, первое из которых - буквально "сокрытие".
А теперь про второе, давайте посмотрим интересный пример (договариваемся сокрытие не использовать, все public).
class RectOrdPoint { // точка на определенной диагонали квадрата public int coordX; public int coordY; public void setCoord(int coord) { this.coordX = coord; this.coordY = coord; } public void setX(int coord) { this.setCoord(coord); } public void setY(int coord) { this.setCoord(coord); } }
Ну вот у нас абстракция над точкой на диагонали в системе координат квадрата. Что мы знаем? При смещении точки по любой оси, она должна сместиться и по другой (иначе перестанет принадлежать этой диагонали).
Вроде все норм, с методами все отлично. Осталось только понять, кто запретит мне, как потребителю этого класса ручками поле поменять? Мы напишем в заголовке "отдельно координаты менять нельзя, координаты должны относиться друг к другу по формуле x=y". Ну вот это буквально "текущая абстракция", которой тут все пугают.
Как же защититься от такой закавыки? Сделать так, чтобы координаты могли меняться только совместно. Ну, например, объявить поля приватными, ибо "нефиг лезть". Я других действительно действующих механик не вижу, а конкретно эта называется "сокрытие".
Вот и выходит, что в отрыве от сокрытия в том или ином виде инкапсуляция в дикой природе почти не встречается. В концепции ООП, особенно в исходной формулировке Алана Кея, именно сокрытие и является краеугольным камнем.
«ООП для меня означает лишь обмен сообщениями, локальное сохранение, и защита, и скрытие состояния, и крайне позднее связывание». Алан Кэй
Из "троицы" наследование-инкапсуляция-полиморфизм в основе ООП только инкапсуляция и есть, причем в виде сокрытия.
nin-jin
23.06.2025 09:40Окей, давайте поанализируем то, что в Вике написано:
Очевидно, тут написана глупость. Читайте предыдущий параграф про отличия инкапсуляции и сокрытия.
кто запретит мне, как потребителю этого класса ручками поле поменять?
А что вам мешает выпрыгнуть в окно?
cupraer Автор
23.06.2025 09:40«Я не понял» ≠ «Очень странный».
qrKot
23.06.2025 09:40Я не понял
Ну, хоть признаешь, уже хлеб)
Инкапсуляция дает гарантию, что никто не сможет изменить внутреннее состояния объекта непредусмотренным способом. Тчк.
Кричать о том, что не работают гарантии, которые вам никто не предоставлял - странное.
Dhwtj
23.06.2025 09:40При несложных объектах (типа счетчика) разницы в скорости почти нет. Затраты и сложность на отправку сообщения в Akka и на захват мьютекса сопоставимы.
При сложных объектах всё кардинально меняется.
И вот здесь абстракция в ООП начинает не просто протекать, а прорывать плотину.
Почему для сложных объектов ООП с блокировками — это боль
Представим сложный объект "Банковский счет" с методом
Transfer(BankAccount other, decimal amount)
.1. Проблема композиции и Deadlock
-
ООП с локами:
Поток А вызывает
account1.Transfer(account2, 100)
. МетодTransfer
внутри себя делаетlock(this)
(т.е. наaccount1
).Далее ему нужно изменить
account2
. Он пытается сделатьlock(account2)
.В это же время Поток Б вызывает
account2.Transfer(account1, 50)
. Он захватываетlock(account2)
и пытается захватитьlock(account1)
.Результат: классический deadlock.
Решение? Ужасное. Нужно вводить глобальное правило порядка захвата блокировок (например, по ID счета). Абстракция "просто вызови метод" сломана. Вам нужно знать о глобальных правилах, чтобы просто перевести деньги.
2. Проблема гранулярности блокировки
-
ООП с локами:
Метод
GetHistory()
должен блокировать весь объект? Или только коллекцию с историей? АChangeAddress()
? АGetBalance()
?Вы начинаете вводить несколько объектов-блокировок внутрь одного объекта:
balanceLocker
,historyLocker
,_addressLocker
.Код превращается в минное поле. Забыли взять нужный лок — получили data race. Взяли не в том порядке — получили deadlock. Абстракция инкапсуляции трещит по швам, наружу торчат детали реализации синхронизации.
3. Проблема блокирующего I/O
-
ООП с локами:
Что если в конце
Transfer
нужно записать транзакцию в базу данных? Делать это внутриlock
— самоубийство. Вы заблокируете оба счета на время долгой сетевой операции.Делать это после
lock
? А если запись в БД упадет? Как откатить изменения в счетах, которые уже "отпустили"? Это требует сложнейшей логики компенсаций.
Как Akka решает эти проблемы для сложных объектов
Akka заменяет прямые вызовы методов асинхронными сообщениями.
Нет Deadlock'ов: Нет блокировок — нет дедлоков. Поток А шлет сообщение
Withdraw
акторуaccount1
. Тот обрабатывает его, меняет свое состояние и шлетDeposit
акторуaccount2
. Никто никого не ждет на блокировке.Гранулярность "из коробки": Каждый актор — это единица гранулярности. Сложная система разбивается на много мелких, простых акторов. Вам не нужно думать о
_balanceLocker
, у вас естьBalanceActor
.Асинхронность "из коробки": Операция с I/O — это просто еще одно асинхронное сообщение. Актор
account1
отправил сообщение вDatabaseWriterActor
и забыл про него. Он свободен принимать новые сообщения. Когда БД ответит (успехом или провалом),DatabaseWriterActor
пришлет ответное сообщение. Это идеально ложится на асинхронную природу реального мира.
Вывод
Для сложных, взаимодействующих систем подход Akka несравнимо лучше, потому что его базовая абстракция (асинхронное сообщение) гораздо лучше моделирует конкурентный мир, чем абстракция ООП (прямой вызов метода), которую приходится "чинить" костылями в виде блокировок.
Вместо вопросов "методGetHistory()
должен блокировать весь объект? Или только коллекцию с историей? АChangeAddress()
? АGetBalance()
?" Akka решает это не улучшением блокировки, а уничтожением самого понятия "сложный объект". Принцип: одна обязанность — один актор. Вы не создаете один гигантский акторBankAccount
, который делает всё. Вместо этого вы разбиваете его на несколько мелких, специализированных акторов.
...... ну в этой части мне самому надо разобраться прежде чем копировать LLM
rsashka
23.06.2025 09:40... ну в этой части мне самому надо разобраться прежде чем копировать LLM
Dhwtj
23.06.2025 09:40боюсь мои комментарии к вашему фейспалму будут слишком токсичными
вы пока ничего толкового не утверждали, показывали только свои вопросы и непонимание
rsashka
23.06.2025 09:40В место того, чтобы подробнее раскрыть свою мысль, вы копируете портянки LLM, которые сами до конца не понимаете. Это вообще нормально? Может мне тоже через LLM вам начать отвечать? :-)
cupraer Автор
23.06.2025 09:40Портянка выше появилась здесь по моей просьбе. @Dhwtj отвечал мне. Предварительно испросив разрешения у автора текста на портянку от ЛЛМ.
Вы забываетесь, юноша.
rsashka
23.06.2025 09:40Ну что-же, самое время напомнить вам про ваш же собственный комментарий
Давайте вы не будете говорить мне, как мне нужно излагать свои мысли, а я в благодарность не стану говорить, куда вам нужно идти.
cupraer Автор
23.06.2025 09:40Это так не работает, я буду всем окружающим сообщать, когда им лучше заткнуться, особенно — в комментариях к моим собственным текстам.
cupraer Автор
23.06.2025 09:40А чё, молодец ваша ЛЛМка, столько воды налить в то, что я в один абзац уместил! Если стало понятнее — я только рад.
Принцип: одна обязанность — один актор. Вы не создаете один гигантский актор
BankAccount
, который делает всё.Вот к этому надо скептически относиться. Как всегда, зависит от ситуации. Иногда бывают довольно развесистые акторы, и с этим ничего не поделать. Но как усреднение по больнице — тоже верно.
Dhwtj
23.06.2025 09:40LLM подстраивается конкретно под меня, мне понятно. Более того, если мне не понятно я прошу переделать ответ. Ну вот так... Наверное те кто больше в теме могут думать более кратко, через красивые абстракции
Смысл не делать гигантский актор в том чтобы не блокировать его целиком надолго. Всё равно блокировки там внутри есть. Неявные, через логические зависимости, но есть
cupraer Автор
23.06.2025 09:40В классической модели — блокировок, как таковых, нет. Есть гарантия, что пока не выполнилась обработка сообщения, новая не запустится. И есть «почтовый ящик сообщений», в который будут падать все входящие.
Вообще, в асинхронных моделях, — блокировка это слово из чужого словаря. Отсылка сообщения мгновенна, а код принято писать так, чтобы синхронных (блокирующих) запросов было бы насколько можно меньше.
qrKot
23.06.2025 09:40Очаровательно-инфантильный подход на особенно удачном примере "банковских транзакций" (к которым вас с вашей ЛЛМ на пушечный выстрел подпускать нельзя)
По порядку:
Представим сложный объект "Банковский счет" с методом
Transfer(BankAccount other, decimal amount)
.Зачем мы его представим? В продакшене мы такое никогда использвоать не будем. У нас будет достаточно простой объект BankTransaction(decimal amount, BankAccount from, BankAccount to). Целиком всю высосанную из пальца проблему решит.
Проблема композиции и Deadlock: ее нет, это транзакция, она одной операцией выполняется. Мы лочим оба баланса разом.
Проблема гранулярности блокировки: ее нет. У нас нет сложного объекта. Есть объект BankAccountBalance, есть BankAccountHistory, есть BankAccountOwner - буковка S из замечательной аббревиатуры SOLID. И мы всегда знаем, что лочить.
Блокирующий I/O... Кхм, тут только цитировать:
Что если в конце
Transfer
нужно записать транзакцию в базу данных? Делать это внутриlock
— самоубийство.Самоубийство - это писать банковскую проводку в БД вне лока транзакции. Вот тут прям настоящее самоубийство.
Предлагаемое вами решение - эпический фейл.
Вы предлагаете Аккаунт1.СписатьДеньги -> Аккаунт2.ЗачислитьДеньги -> ПисательВБД.ЗаписатьВБД. Теперь, внимание, вопрос: с первого аккаунта деньги списались, а Аккаунт2 зафейлился. Чо делаем? А если оба ОК, но БД упала - чо делаем? Если упростить, то буквально:
Как откатить изменения в счетах, которые уже "отпустили"? Это требует сложнейшей логики компенсаций.
Ах, блин, это же в недостатках логики с локами написано! Или ваша модель ортогональна решаемой проблеме (и более того, абсолютнейшим образом не подходит для ее решения)?
cupraer Автор
23.06.2025 09:40Мы лочим оба баланса разом.
Дальше я читать не стал, потому что залочить оба баланса разом в высококонкурентной параллельной среде невозможно.
qrKot
23.06.2025 09:40Беда какая. Пошел требовать удаления
статьи
с вики... И ребятам в Постгрес Про позвоню, расстрою... И банки все обзвоню, передам, что ты сказал расходиться...cupraer Автор
23.06.2025 09:40Нет у меня никакой базы. Я не работаю с проектами, в которых есть база, с этим прекрасно справляются специально обученные хомячки с интеллектом уровня робота-пылесоса.
rsashka
23.06.2025 09:40Нет у меня никакой базы. Я не работаю с проектами, в которых есть база, с этим прекрасно справляются специально обученные хомячки с интеллектом уровня робота-пылесоса.
Что только подтверждает преположение, что у вас не только с ООП все плохо, но и с базами данных толку не хватило разобраться, хотя даже специально обученные хомячки с интеллектом уровня робота-пылесоса с БД прекрасно справляются.
cupraer Автор
23.06.2025 09:40Вас мало повозили вчера мордой по грязи, что вы опять тут вылупились?
rsashka
23.06.2025 09:40Разве? Это вам и вчера и сегодня популярно разжевали, что у вас нет понимания ни в ООП ни в БД (о чем вы сами и написали). А после нескольких высокомерных закидонов с вами и другие нормальные люди перестали общаться и осталось вам только переписываться с LLM :-)
cupraer Автор
23.06.2025 09:40у вас нет понимания ни в ООП ни в БД (о чем вы сами и написали)
Врать-то завязывайте хотя бы. Я нигде такого не писал, хотя бы потому, что это не так.
dzmitry_sidarau
23.06.2025 09:40Вы считаете, что данные могут храниться только в БД и других мест не может быть. Я правильно понимаю? И о долгоживущих Erlang процессах, которые могут быть запущены годами и хранить внутри себя состояние, вы тоже, судя по всему, не в курсе.
Мне очень интересно наблюдать, как людей корёжит, стоит слегка отойти в сторону от привычной парадигмы.
rsashka
23.06.2025 09:40Сам придумал тезис, сам с ним поспорил. Вы уверены, что именно вас таким образом "не корежит"?
dzmitry_sidarau
23.06.2025 09:40Вы что-то явно путаете. Никакой "тезис" я не придумывал. Если вы о долгоживущих процессах, то это придумано и реализовано в виртуальной машине Эрланга аж 40 лет как. И успешно используется начиная от сетевого оборудования и заканчивая джабберами и вотсапами.
rsashka
23.06.2025 09:40Никакой "тезис" я не придумывал.
Ну как же? Вы считаете, что данные могут храниться только в БД и других мест не может быть. Я правильно понимаю?
Я не считаю, что данные могут храниться только в БД.
Место хранения внешних данных, это обычная абстракция с API для обращения к этим самым данным. А уж где они физически хранятся, в БД, процессе Erlang, который живет уже 40 лет в виртуально машине, распределены между разными узлами или вообще вычисляются на лету по мажоритарному принципу, это дело десятое.
cupraer Автор
23.06.2025 09:40Место хранения внешних данных, это обычная абстракция с API для обращения к этим самым данным.
Рыдаю. А чё только одна абстракция, может понадёжней будет ну штук пять взять?
И это, что такое «внешние данные»? Насколько имя пользователя для банковского приложения — внешнее?
qrKot
23.06.2025 09:40И это, что такое «внешние данные»? Насколько имя пользователя для банковского приложения — внешнее?
Вот это вы странное спрашиваете. В контексте беседы "внешние данные" - это все данные, хранящиеся не в памяти приложения.
cupraer Автор
23.06.2025 09:40А если я его храню именно что в памяти приложения, но иногда (при изменениях) записываю в некое персистентное хранилище (не обязательно СУБД)?
Но для всех операций в коде оно хранится строго в памяти приложения?
qrKot
23.06.2025 09:40Надеюсь, для вас не будет откровением, что любой код так или иначе работает с данными, расположенными в памяти приложения. Ну вот не умеет он иначе.
Если есть некие структурированные данные, с которыми вы работаете - это данные в памяти (они же локальные для приложения данные). В случае наличия персистентного хранилища, ну вот просто без вариантов, в этом хранилище будет копия ваших данных. И вот эта копия, получается, будет внешней для приложения.
Не суть есть важно, БД это, диск, лог, очередь - вообще по барабану, это копия, с которой надо так или иначе синхронизироваться. Вы можете брать значение из памяти и отливать в хранилище или спрашивать из хранилища значение и класть в память - вопрос только в том, кто "источник истины". Суть от этого не изменится, для работающего приложения значение в хранилище будет внешним.
Причем, тут уже без разницы, ФП/императивный подход. Надеяться, что ФП магическим образом само как-то решит за вас проблему консистентности данных - какая-то наивность. В случае любой программы есть данные в памяти, и данные за ее пределами.
cupraer Автор
23.06.2025 09:40Надеяться, что ФП магическим образом само как-то решит за вас проблему консистентности данных - какая-то наивность.
Конечно. А к чему этот пассаж?
Если это как-то касается меня и моего текста — я не фанат ФП, не топлю за ФП, и не считаю ФП панацеей от всех бед.
qrKot
23.06.2025 09:40Вот тут вы несколько несправедливы. Беседа таки в ключе исходной статьи ведется, а в статье таки в качестве примера BankAccount приводится. И вот хранить стейт банковских счетов в "долгоживущих Erlang процессах" (т.е. инмемори, я же верно понимаю) - ну вот такая себе идея.
В исходном ключе обсуждения таки да, данные о состоянии банковских счетов могут храниться только в БД. Мало того - еще и обязательно в транзакционной!
cupraer Автор
23.06.2025 09:40Не ткнёте пальчиком в то место исходного текста, где упоминается банковский аккаунт? Заранее спасибо.
И это, есть куча примеров хранения счетов в ивентлогах. Например, потому что так безопаснее.
cupraer Автор
23.06.2025 09:40Каких еще счетов, нафиг? Где в исходном тексте хоть одно слово про счета?!
cupraer Автор
23.06.2025 09:40в статье таки в качестве примера
BankAccount
приводитсяТак чего, Господин Фантазёр, покажете, где именно в тексте я хоть что-то говорил про банковсие аккаунты, или признаете, что заврались вконец?
qrKot
23.06.2025 09:40Ну и с понятием транзакции вы, видимо, тоже не знакомы. И реальных проектов не писали...
Зато мнение имеете.
cupraer Автор
23.06.2025 09:40Реальных проектов без базы с транзакциями не бывает? Ясно.
qrKot
23.06.2025 09:40Ну вы же сами в пример привели класс BankAccount! И, внезапно, проектов по управлению балансами банковских счетов без базы с транзакциями - действительно не бывает.
Был бы другой пример, другой бы разговор был. А для банковских счетов есть 2 неотъемлемых требования:
Транзакционность
Последовательность операций.
Требование параллельно 2 операции по одному счету выполнять - ересь, высосанная из пальца. На банковские транзакции ФП ложится отвратительно, не надо его тащить. А ООП вы не понимаете, от слова "вообще"
cupraer Автор
23.06.2025 09:40Давайте вы отучитесь говорить за всю сеть. Вы не видели банковских счетов вне транзакционных баз данных? — Бывает, люди не рождаются на свет с широким кругозором.
SolidSnack
23.06.2025 09:40Приведите пример счетов и банков без транзакций(что бы я ими не пользовался)
Dhwtj
23.06.2025 09:40любая с конечной согласованностью подойдет и событийная туда же, "счета и банки" бывают с разными требованиями
qrKot
23.06.2025 09:40любая с конечной согласованностью подойдет
Буду краток: нет.
и событийная туда же
Еще раз: нет.
банки" бывают с разными требованиями
И еще раз: нет.
Dhwtj
23.06.2025 09:40банки это не только перекладывание денег - в перекладывании со счета на счет я бы поостерегся от конечной согласованности
выражайтесь точнее
qrKot
23.06.2025 09:40Да блин куда уж точнее: "проектов по управлению балансами банковских счетов"
Управление балансами (читай, изменение циферки на счету) банковских счетов.
Ну ок, сократим формулировка: банковские транзакции.
nuclight
23.06.2025 09:40Почему же? На самом деле она ровно такая и есть, несмотря на то, что внутри у (каждого участвующего) банка Oracle с транзакциями. Ну вот просто открываю я детализацию по своим покупкам, если мне интересно, и вижу, что деньги за вчерашнюю еду в супермаркете на самом деле висят в статусе HOLD. Их отпустит, когда пройдет клиринг (или как-то так, не помню уж точно). То есть для конечных юзеров - что меня, что магазина - создают впечатление, что деньги перечислились мгновенно и транзакционно. По сути, это оптимистический подход в создании видимости, а на самом деле там работает более сложная логика, которая может откатить это обратно через пару суток (а то и больше, если кто-то откроет диспут).
Выражаясь иначе, если разные банки и так друг другу выступают как акторы, никто не мешает делать того же самого для счетов внутри банка.
Dhwtj
23.06.2025 09:40Их отпустит, когда пройдет клиринг
вроде, от такого отказываются уже
а то можно вспомнить понятие «банковский рейс» — комплекс операций по приёму и обработке платежей, которые проводятся несколько раз в день в соответствии с определённым графиком.При межбанковских переводах Центробанк не переводит деньги моментально, а совершает их перевод только в определённые промежутки времени. Поэтому средства могут поступить на счёт не сразу, а в течение дня или даже нескольких рабочих дней.
nuclight
23.06.2025 09:40Почему отказываются, если это основа работы международных платежных систем типа той же Visa? Остальное про Центробанк звучит как что-то вообще докомпьютерной эпохи.
Dhwtj
23.06.2025 09:40Могу только примерно. Я мало отношения к банкам имею.
Можно делать платежи напрямую (золотом, гыгы), а чтобы на каждый платёж не возить золото можно делать расчет через клиринговый центр, который осуществляет как бы кэширование записей и реальные расчёты в конце торгового периода, дня или рейса (лет 300-400 назад это был физический рейс с золотом). Между странами специальные клиринговые центры, внутри через ЦБ.
Каждая транзакция через ЦБ обрабатывается индивидуально (это и есть «валовые расчеты» / gross settlement). Это долго и дорого, применяется к большим суммам.
Небольшие платежи ЦБ может реально завершать быстро, через систему быстрых платежей. Такие есть ещё свои у крупных банков. Ну и на закуску прямые переводы (неттинг), если банки доверяют друг другу а потом рассчитываются через клиринг.
ЦБ это гарант и финальное юридическое подтверждение платежа. На нём транзакция заканчивается. А все эти быстрые переводы к которым вы привыкли это ещё не завершенная транзакция, платеж можно оспорить и вернуть.
Как работают карточные платежи и палка и почему такой платеж можно оспорить чуть ли не спустя месяц - я не знаю.
nuclight
23.06.2025 09:40Я тоже не знаю во всех деталях (чисто с какими тасками сталкивался, и это еще до даже идеи СБП было), но в контексте обсуждения достаточно того, что в большом масшабе нет никакого "лочим обе записи сразу в рамках одной транзакции", а делается по вполне акторной модели.
Dhwtj
23.06.2025 09:40Лочим обе записи это если между своими счетами в одном банке. Да и то, не лучшее решение
cupraer Автор
23.06.2025 09:40А вы устанавливаете транснациональные законы написания банковского ПО, или просто попердеть в лужу зашли?
Потому что ну вот я скажу: да, и еще раз да. В РФ мне с банками работать не доводилось, зато могу много порассказать про Европу и США.
Dhwtj
23.06.2025 09:40Eventual consistency?
Там есть ненулевая вероятность не прийти к согласованности никогда. Хотя, вероятность может быть очень низкой и приемлемой далее в финансах. Но надо уметь считать риски. Интересно, как оно там
cupraer Автор
23.06.2025 09:40Да вот ровно так.
До определенного порога платежа — просто лог, для списаний, которые выше этого порога — пин на терминале + настоящая транзакция.
SolidSnack
23.06.2025 09:40Странно как-то, почему-то в статьях они обсуждают что выбрать SQL или NoSQL, ну и очевидно пишут что SQL с его транзакциями лучше всего подходит для обслуживания денежных переводов)
SolidSnack
23.06.2025 09:40Все забываю что у вас гуглеж сломался, там даже графики сравнения транзакций и других аспектов есть)
cupraer Автор
23.06.2025 09:40А, всемирно известная Priyanka Gowda Ashwath Narayana Gowda, в своей курсовой работе? — Тогда конечно.
SolidSnack
23.06.2025 09:40Получается у них в половине вузов учат использовать SQL и NoSQL, а другая половина изучает ваш подход?))) (исходя из вашего утверждения что каждый 2 банк без баз данных в америке)
cupraer Автор
23.06.2025 09:40Вы бы для начала попытались понять, в чем заключается мой подход. Это не так сложно.
Dhwtj
23.06.2025 09:40В продакшене мы такое никогда использвоать не будем
Лейстрид тоже был очень полезен как человек начисто лишенный воображения ©
Это игрушечный пример. Но уровень дискуссии такой, что все просто рефлекторно
кидаются какашками в оппонентабыстро и не совсем точно отвечают.Проблемы ООП с многопоточностью начитаются с определенного уровня сложности модели. На простых можно решить вручную. На сложных придется использовать ФП или акторную модель.
Простой пример от автора тоже не годный.
nuclight
23.06.2025 09:40...только хотел плюсануть (всё верно, за вычетом того, что я не знаю Akka), и тут ВНЕЗАПНО всё портит LLM.
Dhwtj
23.06.2025 09:40ладно, не буду больше в LLM
буду гуглить и постить другую глупость, человеческую
почему гуглить? уж очень термины все в программировании нечеткие, просто бесит
"по определению дяди Боба микросервис то, а по определению тети Сони сё"
nuclight
23.06.2025 09:40Увы, такова природа человека, это уже к психологии и мозгу, с которых всё на самом деле и начинается, а не с формальной математики...
cupraer Автор
23.06.2025 09:40Это годная LLM, раз в год и палка стреляет :)
https://habr.com/ru/articles/920898/comments/#comment_28475174
-
Dhwtj
23.06.2025 09:40Как обсуждение ООП так все бросают друг в друга ссаными тряпками
Да чтож такое опять?
Лол
rsashka
23.06.2025 09:40Хм, ну ведь можно же без портянок LLM мысли формулировать? :-)
Причем под ними я подписываюсь на все 146%Dhwtj
23.06.2025 09:40LLM я знаю что читать не очень приятно, о чём я предупредил. Но тяжело сформулировать сложную мысль на бегу. Я же не сижу за компом целый день. Не хотите не читайте. Всё честно.
А вы не изволили сформулировать ничего
rsashka
23.06.2025 09:40Предпочту вообще ничего не читать, чем читать тонны LLM мусора в комментариях. Тем более, когда автор сам не разобрался, что же он там нагерерировал.
HiItsYuri
23.06.2025 09:40Удваиваю. Ощущение, что за несколько недель комментарии на Хабре превратили в склад нейрослопа, где каждый неразобравшийся в теме гадит выводом ллмок. Спорить с ними толку нет, ибо знаний в них нет, и то что написала ллм они не понимают/не видят ошибок.
cupraer Автор
23.06.2025 09:40Причем, я там в тексте в двух местах намеренно подставился, и люди, умеющие в ООП, должны были бы атаковать эти уязвимости, но нет: пока цепляются к пуленепробиваемым аргументам.
Dhwtj
23.06.2025 09:40Что забавно: в однопоточных языках (JS, python, PHP) объекты не очень-то и нужны. Они там выглядят как что-то инородное.
cupraer Автор
23.06.2025 09:40Я обожаю руби, в котором буквально всё — объект (даже примитивных типов нет). Там объекты выглядят как влитые, если что.
Но вот именно руби не претендует на все эти умные паттерны типа «инкапсуляции» и прочих. Там даже приватные методы можно задокументированными средствами языка вызывать извне. Можно подменить класс
Integer
, чтобы он хранил текстовые представления строк на суахили. Можно буквально всё. И именно за это я его люблю. Вот в такой парадигме — ты программист, ты умный, ты разберёшься, и ты ответишь, если что — мне ООП очень нравится.
nin-jin
23.06.2025 09:40Ну приехали. И как же писать на JS без объектов?
Dhwtj
23.06.2025 09:40Изначально прототипное наследование. ООП (классы) только потом прикрутили
Ок, объекты были. Классов изначально не было.
cupraer Автор
23.06.2025 09:40Технически, JS разделяет объекты и массивы, а массивы, очевидно, диффеоморфны объектам. Voilà.
nin-jin
23.06.2025 09:40Вот как раз технически они ничем не отличаются.
cupraer Автор
23.06.2025 09:40Семантика добавления элементов абсолютно разная, и, как следствие, объект тащит за собой хэш-таблицу, которая массиву не нужна. Размещение в памяти разное, прототипы разные. Но, конечно, можно трактовать слово «технически» достаточно широко, например: и те и другие являются примитивами языка.
nin-jin
23.06.2025 09:40Вы опять рассуждаете о том, о чём не имеете ни малейшего представления. Как таковых массивов в JS вообще нет. Есть лишь объекты с особым поведением свойства length и куча костылей в компиляторе, чтобы это не так сильно тормозило в большинстве случаев.
cupraer Автор
23.06.2025 09:40Я человек простой: пошел да посмотрел в код ноды. Посмотрите, как
v8::Array
в принципе реализован; но, конечно, может быть я всё неправильно понял, и разучился читать код.nin-jin
23.06.2025 09:40Ага, давайте судить о языке по одной из множества его реализаций, ссылаясь на первую попавшуюся хелперную функцию в стороннем проекте.
Zulu0
23.06.2025 09:40В былые времена для большого количества потоков мы использовали веблоджик, код под которым был однопоточный, но сервер умел раскидывать его по нодам и воркерам, создавая упрощенную модель. На данный момент это дорого, да и специалисты умеющие в работу с этим инструментом кончились, сейчас по хайпу пилить микросервисы, положив большой прибор на архитектуру с учетом ddd, и логику работы оркестрации процеса в угоду непонятного жонглирования в жрпц сервисах портами и урлами. Дальше выводы делайте сами...
cupraer Автор
23.06.2025 09:40Если честно, мне не очень понятно, какие именно выводы предлагается сделать.
Мир катится в тартарары? — Дык тоже мне новость.
digrobot
23.06.2025 09:40А давайте двум плотникам дадим один топор, и заставим их делать один табурет? Да не, пробовали, они дерутся все время за топор, мешают друг другу. Ну может дадим им второй топор? Еще хуже стало - пальцы друг другу поотрубали... Мораль-то в чем? А, виновато ООП.
cupraer Автор
23.06.2025 09:40Если вы не понимаете разницу между виновато и непригодно — мне жаль.
digrobot
23.06.2025 09:40Топор тоже непригоден для бритья, однако же остается отличным инструментом для своих задач, и эти задачи никуда не делись в 2025. Многопоточный хайлоад это до сих пор узкий сегмент, для которого свои технологии. Хотя вот та же винда наполовину написана на ООП с мьютексами, и ничего, работает.
cupraer Автор
23.06.2025 09:40Я нигде не говорил, что существуют технологии, на которых невозможно написать работающий код.
В тексте заявлено буквально следующее: в многопоточной среде больше половины так называемых «гарантий», краеугольных камней ООП, типа инкапсуляции и двух третей паттернов — превращаются в тыкву. Если вас это устраивает — флаг в руки.
Мне доводилось писать многопоточный довольно конкурентный код на руби с ручной обработкой пула потоков на си, и оно тоже работало. Понравился ли мне этот опыт и хочу ли я его повторить? — Нет, спасибо.
digrobot
23.06.2025 09:40ООП решает другие проблемы. Инкапсуляция - для ограничения области видимости. Наследование - для переиспользования кода. Полиморфизм - для унификации интерфейсов. Никогда, нигде я не встречал обещаний, что ООП обеспечивает синхронизацию доступа к данным.
Dhwtj
23.06.2025 09:40Инкапсуляция - для ограничения области видимости
нет, вы читали не те учебники
Инкапсуляция это не столько инструмент (ограничение видимости), сколько цель (защита изменяемого состояния для соблюдения правил,инвариантов).
Этот спор — это отражение эволюции языков. От "вот инструменты, делай правильно" к "ты не сможешь сделать неправильно"
DarthVictor
23.06.2025 09:40Вашу проблему с плотниками Генри Форд ещё 100 лет назад решил.
Кстати вполне себе в ФП стиле.
qrKot
23.06.2025 09:40При этом бухгалтера у Форда как из одной книжки в другую циферки переписывали последовательно, так и продолжили переписывать. Ну потому что требования транзакционности еще есть...
cupraer Автор
23.06.2025 09:40Это ивентлог, безтранзакционный, на котором работают многие современные банки.
Потому что люди делятся на тех, кто видел ситуацию «стартовали из приложения транзакцию и тут отвалилась сеть», и тех, кто еще нет.
SbWereWolf
23.06.2025 09:40Автор у вас проблемы не с ООП, а с требованиями к коду. Если код должен что то безопасно сделать, то вы в своём коде должны эту безопасность обеспечить.
У вас требование безопасности есть, а в коде выполнения этого требования нет.
Если мы должны атомарно обновить значение при выполнении какого условия, то код который выполняет это обновление должен выполнять его атомарно, то есть проверить что условие выполнилось и только в этом случае проводить обновление значения.
В SQL для этого делается блокировка записи через SELECT FOR UPDATE, или можно делать апдейт на конкретное значение, с проверкой того что текущее значение не изменилось:
UPDATE balance SET amount=150 WHERE amount=200 and user_id=123
Как то так надо выполнчть изменение данных. Если это переменнная в памяти, или байты в файле, то изменение значения надо выполнять с аналогичными проверками. Условие выполняется ? Да - тогда обновляем. А что бы ни какой другой поток переменную не поменял используем блокировки, вы это прекрасно знаете.
ООП это метод. Вы его применяйте с учетом всех требований и программа будет работать как надо.
А если ваш код не обеспечивает атомарности, то пеняйте сами на себя, а не на ООП.
ООП это только способ установить границы того как можно изменять данные, ООП это подсказка программисту какие операции над данными можно выполнить. ООП не говорит нам как эти операции должны быть выполнены.
Вообще поэтому объекты и нельзя использоаать как DTO, именно поэтому мы должны сказать объекту: уменьши баланс на 50, если текущий баланс 200. И объект сам разберется как ему это сделать атомарно с транзакционной целостностью.
Говорить объекту: установи баланс 150, это значит брать на себя ответственность за то что баланс должен быть 150. Когда вы так используете объект, то это не ООП, это процедурное императвное программирование.
cupraer Автор
23.06.2025 09:40Комментатор, у вас проблемы с пониманием несложных текстов.
ООП гарантирует инкапсуляцию? Если вы согласны, что нет — тогда у нас нет разногласий.
qrKot
23.06.2025 09:40Инкапсуляция, пожалуй, единственный неотъемлемый компонент ООП. Под инкапсуляцией подразумевается, как правило, ограничение доступа к внутреннему состоянию объекта извне. Остальное - наносное.
Вот это - гарантируется. Что не так?
ruslan_sverchkov
23.06.2025 09:40ООП не гарантирует инкапсуляцию, ООП предоставляет некоторые средства, которыми можно достичь инкапсуляции (если хватит квалификации). Достичь ее можно и без ООП, с ООП просто удобнее. Как вы вообще себе представляете технически гарантию инкапсуляции в языке, в котором юзер может писать свои классы)
cupraer Автор
23.06.2025 09:40Как вы вообще себе представляете технически гарантию инкапсуляции в языке, в котором юзер может писать свои классы)
Да запросто: например, полная иммутабельность.
ruslan_sverchkov
23.06.2025 09:40С полной иммутабельностью очень тяжело написать что-то полезное, состояние программы должно меняться, то что фп для этого изобретает всякие трюки глобально ничего не меняет. И даже если бы меняло - инкапсуляция не только про изменение состояния, но и про чтение того, что читаться не должно (ака деталей реализации).
cupraer Автор
23.06.2025 09:40Достаточно ли полезен Whatsapp? А Discord?
Кроме того, я просто привел один из тонны примеров в ответ на вопрос «Как вы вообще себе представляете технически гарантию инкапсуляции в языке, в котором юзер может писать свои классы».
ruslan_sverchkov
23.06.2025 09:40У Whatsapp и Discord вообще нигде нет изменяемого состояния? Ни одной бд не используется? (она тоже изменяемое состояние)
один из тонны примеров
спасибо конечно, я тоже могу себе представить программу в которой ничего не меняется) Давайте я перефразирую если это действительно было неочевидно - есть огромный класс программ, в которых состояние должно меняться, и язык, если он не хочет быть узконишевым, должен это поддерживать
cupraer Автор
23.06.2025 09:40Иммутабельность — это свойство объектов языка, а не языка в целом. В полностью иммутабельном языке можно запросто менять состояние.
Зато на неиммутабельном языке не построить, например, конечный автомат с соблюдением гарантий.
ruslan_sverchkov
23.06.2025 09:40везде где можно менять состояние, можно написать такой код, который позволит менять это состояние тому, кто это делать не должен теми способами, которыми это делаться не должно) про это и есть инкапсуляция, а не про конкретные способы, которыми этот эффект достигается
cupraer Автор
23.06.2025 09:40везде где можно менять состояние, можно написать такой код, который позволит менять это состояние тому, кто это делать не должен теми способами, которыми это делаться не должно
Не позорьтесь. Почитайте хоть немного про акторную модель, а потом ввязывайтесь в такие дискуссии.
ruslan_sverchkov
23.06.2025 09:40Не позорьтесь
Хорошо, больше не буду)
Попробуйте включить абстрактное мышление и понять что делает акторная модель глобально, чем она фундаментально отличается от неакторной модели (может быть поможет подумать во что это в конечном счете компиляется и на чем исполняется), подумайте про Тьюринга vs Черча. Не знаю как ещё донести до вас элементарные вещи)
cupraer Автор
23.06.2025 09:40Вам вряд ли удастся донести до меня то, что полнота по Тьюрингу гарантирует вседозволенность, или что изоморфность всех типов лямбдам — как-то специальным образом раскрывает все данные.
Язык может быть триста раз полным по Тьюрингу, но если кусок кода работает с базой по read-only соединению, то поменять состояние ему не удастся. Внезапно, правда?
Dhwtj
23.06.2025 09:40Инкапсуляция должна включать гарантии потокобезопасности. И в ООП это сложно, компилятор не гарантирует.
Dhwtj
23.06.2025 09:40из определения
вот видите, вы даже не видите
цель инкапсуляции - защита инвариантов (и сокрытие реализации, например заменой публичных полей на геттеры и сеттеры под защитными правилами)
В многопоточной среде, если объект не потокобезопасен, любой вызов его публичных методов может привести к гонке данных и разрушить инварианты. Защита не сработала.
qrKot
23.06.2025 09:40из определения
Покажите мне уже это определение, на которое вы ссылаетесь. Я ни одного подходящего не видел.
цель инкапсуляции - защита инвариантов
Нету у нее такой цели.
У вас очень странный подход: придумывать какие-то цели и гарантии, которых изначально никто не обозначал, и сетовать, что они не работают. Ссылаться на несуществующие определения - тоже отличный план.
Я вам сейчас страшное скажу: в определении ФП тоже ничего про потокобезопасность нет. Оно вообще не для того придумывалось, и не те проблемы решало. Потокобезопасность - это следствие из ограничений, в которых существует функциональное программирование (вы не можете сломать разделяемое состояние, если у вас его нет).
И вы, ссылаясь на следствие из парадигмы ФП, утверждаете, что оно же должно быть заложено в определении ООП? Вы в своем уме вообще?
Emelian
При реализации собственных пет-проектов на C++ / WTL, использование многопоточности не привело, лично у меня, к каким-то особым проблемам в использовании ООП (см. скриншот моей неопубликованной программы «МедиаТекст»: http://scholium.webservis.ru/Pics/MediaText.png ). Да, менеджер событий можно было бы оптимизировать, но менеджер потоков там работал нормально.
Другой мой проект (см. мою статью: «Новая компьютерная программа для запоминания иностранных слов и фраз» в https://habr.com/ru/articles/848836/ ) использовал, в том числе, эмуляцию видео-режима. Не знаю, можно ли считать работу с таймером многопоточностью либо его эмуляцией, но проблемы там были не в ООП. Наоборот, решение своих проблем я связывал с «правильным» использованием ООП.
Как бы там ни было, но для пет-проектов это вполне работает.
cupraer Автор
Позволю себе предположить, что в пет-проектах было не так много высоконкурентного хайлоада.
Emelian
Естественно! Иначе, говорил бы о профессиональных проектах. Но, к сожалению, я только любитель…