Комментарии 157
Конкуретность сама по себе противоречит инкапсуляции
Инкапсуляция не скрывает наличие и влияние состояния. Она запрещает небезопасный способ работы с ним.
блокировать монетоприёмник и считыватель карт пока турникет открыт - это штатное поведение и инкапсуляции не противоречит
Это еще почему?
Поправил выше
ООП не при чём
Вторая оплата выдаст ошибку. И ваше железо изначально должно иметь возможность обработать ошибку штатно.
блокировать монетоприёмник и считыватель карт пока турникет открыт — это штатное поведение и инкапсуляции не противоречит
Не-а. Потому что если подумать чуть дальше, то рано, или поздно, оно заблокируется не тогда, когда нужно, и чтобы его разблокировать — придется лезть грязными руками во внутренности имплементации конечного автомата. Или «всё перезапустить» (по этому пути [пока] успешно развивается кубернетис).
Это лишь означает, что ваш софт не дописан и вы не учли ещё одного состояния. Каким образом недописанный код является нарушением инкапсуляции - для меня загадка. Да и вообще по статье аргументация от ложного позволяет получить абсолютно любой результат. Ну и в "том самом" ООП подразумевается, что всё общение происходит посредством сообщений/событий между объектами и все ваши примеры "нарушений" инкапсуляции фактически должны исчезнуть с появлением таких событийных моделей.
Только ваши примеры примерно никак этого не отражают и обработка/валидация данных у вас превращаются в нарушение инкапсуляции, в то время как в каноничном ООП это было бы просто ещё одно сообщение в очереди сообщений.
В каноничном ООП, не существующем в окружающем нас мире в представлении Алана Кая, но реализованном «примерно по лекалам» Джо Арсмтронгом в эрланге (о чем я еще написал выше) — нет никаких абстракций, сложнее синхронных сообщений и нет никаких паттернов.
Да вам вроде больше и не нужно. Взял сообщение из очереди, разослал валидным адресатам, опционально дождался ответов, перешёл к следующему в очереди.
Я вот все еще не понял, почему выброс исключения это кишки наружу? потому что вы изначально о требовании "температура не более 273.15" не написали? ну какбы проблема тогда ищется не там.
Не нравится вам исключение - напишите обертку над double, которая будет представлять собой число double гарантированно укладывающееся в требуемый диапазон, и принимайте в сеттере ее.
Вам в любом случае нужно предоставлять какие-то рамки использования для потребителей вашей библиотеки - если любые рамки это "кишки наружу" то кажется вы ситх.
Дело не в рамках, а в том, что, как я написал прямо в тексте, под которым вы комментируете, вызывающий код не знает (и не может знать из-за инкапсуляции), что делать с исключением. Обернуть double
— идея на первый взгляд получше, но тогда вы раскрываете детали имплементации, что, в свою очередь, не позволит вам завтра изящно начать принимать фаренгейты.
вызывающий код не знает (и не может знать из-за инкапсуляции), что делать с исключением
Как всегда с исключением - передаёт тому кто знает. Исключение: переданное значение температуры не имеет физического смысла. Господа вызвавшие, сами разбирайтесь кто у вас там лох. Если никто из вызвавших не подумал что такое может быть (несмотря на то что в описании метода написано \throws logical_error при бессмысленных значениях
), ваши проблемы. Инкапсуляция-то тут при чём? Инкапсуляция скрывает, храню я значение как градусы Цельсия, как статистическое распределение электрон-вольтов, как ссылку на аппаратный регистр, или ещё каким-нибудь интересным образом. Но исключения - часть интерфейса.
Ерунда какая-то. Вы пишете так, будто бы изначально можно было принимать любое значение температуры включая обе бесконечности, да еще и NaN в придачу, а потом вдруг программист понял, что сделал хрень, поскольку вычислять с этими значениями не получается, а цифровой индикатор не способен это отобразить и решил валидацию данных (наконец-то) по-человечески сделать и тут это вдруг нарушило инкапсуляцию. Во-первых, если это Java, то там список исключений в методе включен в контракт. Во-вторых, по вашему любая бизнес-логика и любые ограничения в контракте нарушают инкапсуляцию?
Ну и при чем здесь прием фаренгейтов? Если вы готовы принимать температуру в нескольких единицах, то будьте добры описать ограничения для каждого контракта и описание ошибок/исключений, которые контракт отдает (лучше единообразно, к примеру), если ему пытаются впихнуть невпихуемое. Но назвать это нарушением инкапсуляции - вот это уже просто нонсенс.
Пример с турникетом из той же оперы. Вы как будто бы пытаетесь навернуть софтверную логику своего конечного автомата с большим количеством состояний на железный автомат, который этих состояний не имеет. В общем случае это не получится, разумеется. Если к примеру монетоприемник не имеет свойства блокировки или какого-нибудь другого механизма, типа возврата монеты, то тут хоть обпрограммируйся, нормального решения не будет. И проблема здесь лежит далеко за пределами ООП в принципе.
Слышали что-нибудь про возвращаемое значение типа `Result<T, E>`, обработка ошибки которого принуждается системой типов и мимо которой невозможно пройти? Что там вызывающий код не знает?
Вероятно речь о том, что изменение внутреннего поведения может добавить новый класс ошибок, а значит нарушить контракт.
Ну то такое...
Появляется новый тип ошибки: "уже активирован другой проход". Если вы готовы его обработать новым способом, то вводите. Если не готовы, то это будет просто "неопознанная ошибка"
Если не готовы, то это будет просто «неопознанная ошибка»
И как нам это поможет в реальной жизни, когда оператора, который может это прочитать и дернуть рубильник — нет?
Оплата не пройдет и всё
Я не уверен, сколько бы мне удалось проработать на моем текущем месте после предложения так реагировать на ошибки, но меньше одного дня — это несомненно.
Что предлагаете?
Решений довольно много, но они все зависят от бизнеса и придумываются не мной, я только реализую. Например, можно сохранять все ошибки в стейте, не меняя собственно значение, и опрашивать наш процесс раз в час на предмет ошибок. Можно прикрутить так любимый мной listener. Можно писать всё в event log. Можно просто падать, пусть супервизор работает, он железный. Зависит.
В конкретном случае с турникетом и оплатой — надо копить стейт, как я написал прямо в тексте, чтобы можно было бросить 10 жетонов, а потом пройти вдесятером.
Очевидно, расширение поведения потребовало расширения обязанностей вызывающего кода, железа или на что вы там жаловались
логика в накоплении чудесная но ровно до того случая когда один человек при оплате картой не успеет убрать эту самую карту и система посчитает что приложена следующая карта / он хочет оплатить за двоих и сделат второе списание
В конкретном случае с турникетом и оплатой — надо копить стейт, как я написал прямо в тексте, чтобы можно было бросить 10 жетонов, а потом пройти вдесятером.
Так это же и есть инкапсуляция.
Вот у вас есть Турникет.
У него есть два состояния - Открыт, Закрыт.
И события - Жетон, Карта, Пассажир.
Есть внутренние данные (инкапсулированные) - Кол-во проходов.
Работает просто:
Открыт + Жетон или Карта -> Открыт (кол-во проходов +1)
Закрыт + Жетон или Карта -> Открыт (кол-во проходов +1)
Открыт + Пассажир -> Открыт или Закрыт (в зависимоти от врутренних/инкапсулированных данных)
Закрыт + Пассажир -> Исключение!
Вот и получается, что наружу "кол-во проходов" не торчит, т.е. инкапсулированно.
При этом вролне нормально иметь метод displayInfo
где мы можем показать сколько проходов отсталось. Или добавить метод reset
если у нас бывают какие-то ситуации, что турникет заглючил и его нужно перезагрузить.
Я подробно разобрал этот случай прямо в тексте. Там в ООП пять паттернов надо применить, чтобы это заработало.
Состояния Конечного Автомата:
ЗАБЛОКИРОВАНО (Locked / Closed): Начальное и конечное состояние нормального цикла. Ожидает оплаты. (Соответствует: индикатор красный, механизм заблокирован, таймер выключен).
РАЗБЛОКИРОВАНО (Unlocked / Open / Paid): Оплата произведена, проход разрешен в течение ограниченного времени. (Соответствует: индикатор зеленый, механизм разблокирован, таймер запущен).
События (Inputs):
ЖЕТОН: Вставлен жетон.
КАРТА: Приложена карта.
ПРОХОД: Зафиксирован проход человека.
ТАЙМАУТ: Истекло время ожидания прохода.
НЕВАЛИДНЫЙ_ВХОД: Попытка оплаты невалидным жетоном или картой с ошибкой.
ПОПЫТКА_ПРОХОДА_БЕЗ_ОПЛАТЫ: Физическая попытка пройти через заблокированный турникет.
Переходы и Действия (Transitions & Actions):
Формат: ТекущееСостояние + Событие -> НовоеСостояние + [Действия / Выходные сигналы]
1. Из состояния ЗАБЛОКИРОВАНО:
ЗАБЛОКИРОВАНО + ЖЕТОН (валидный) -> РАЗБЛОКИРОВАНО + [Принять жетон, Разблокировать механизм, Запустить таймер, Включить зеленый индикатор]
ЗАБЛОКИРОВАНО + КАРТА (успешная оплата) -> РАЗБЛОКИРОВАНО + [Подтвердить оплату, Разблокировать механизм, Запустить таймер, Включить зеленый индикатор]
ЗАБЛОКИРОВАНО + НЕВАЛИДНЫЙ_ВХОД (жетон/карта) -> ЗАБЛОКИРОВАНО + [Отклонить жетон/карту, Вернуть жетон (если применимо), Оставить красный индикатор] -> Сигнал: ОШИБКА_ОПЛАТЫ
ЗАБЛОКИРОВАНО + ПОПЫТКА_ПРОХОДА_БЕЗ_ОПЛАТЫ -> ЗАБЛОКИРОВАНО + [Оставить механизм заблокированным, Оставить красный индикатор] -> Сигнал: ТРЕВОГА / ОШИБКА_НЕСАНКЦ_ПРОХОД
ЗАБЛОКИРОВАНО + ПРОХОД -> (То же, что и ПОПЫТКА_ПРОХОДА_БЕЗ_ОПЛАТЫ)
ЗАБЛОКИРОВАНО + ТАЙМАУТ -> ЗАБЛОКИРОВАНО + [Ничего не делать (таймер не был запущен)]
2. Из состояния РАЗБЛОКИРОВАНО:
РАЗБЛОКИРОВАНО + ПРОХОД -> ЗАБЛОКИРОВАНО + [Остановить таймер, Заблокировать механизм, Включить красный индикатор] -> Сигнал: (Успешный проход завершен)
РАЗБЛОКИРОВАНО + ТАЙМАУТ -> ЗАБЛОКИРОВАНО + [Заблокировать механизм, Включить красный индикатор] -> Сигнал: ОШИБКА_ТАЙМАУТ
РАЗБЛОКИРОВАНО + ЖЕТОН -> РАЗБЛОКИРОВАНО + [Отклонить жетон, Вернуть жетон, Оставить зеленый индикатор] -> Сигнал: ОШИБКА_УЖЕ_ОПЛАЧЕНО
РАЗБЛОКИРОВАНО + КАРТА -> РАЗБЛОКИРОВАНО + [Отклонить карту, Оставить зеленый индикатор] -> Сигнал: ОШИБКА_УЖЕ_ОПЛАЧЕНО
РАЗБЛОКИРОВАНО + НЕВАЛИДНЫЙ_ВХОД -> РАЗБЛОКИРОВАНО + [Отклонить жетон/карту, Оставить зеленый индикатор] -> Сигнал: ОШИБКА_УЖЕ_ОПЛАЧЕНО (или ОШИБКА_НЕВАЛИДНЫЙ_ВХОД, но первая причина важнее)
РАЗБЛОКИРОВАНО + ПОПЫТКА_ПРОХОДА_БЕЗ_ОПЛАТЫ -> (Зависит от датчиков. Если это просто второй человек пытается сразу пройти, может сработать тревога, но состояние может остаться РАЗБЛОКИРОВАНО до ПРОХОД или ТАЙМАУТ. Или может сразу блокироваться для безопасности). Давайте для простоты считать, что основной переход - это ПРОХОД одного человека. Повторные попытки в состоянии РАЗБЛОКИРОВАНО могут игнорироваться до перехода в ЗАБЛОКИРОВАНО или вызывать отдельную тревогу без смены состояния FSM).
Ключевые моменты этой модели:
Единое состояние: В любой момент времени турникет находится ровно в одном из определенных состояний (ЗАБЛОКИРОВАНО или РАЗБЛОКИРОВАНО).
Атомарность Переходов: Обработка события и переход в новое состояние (включая все действия) должны быть атомарны. Это критично для обработки конкурентных событий (одновременные ЖЕТОН и КАРТА), где только одно событие должно успешно перевести систему из ЗАБЛОКИРОВАНО в РАЗБЛОКИРОВАНО. Второе событие придет уже в состояние РАЗБЛОКИРОВАНО и будет обработано по правилам этого состояния (т.е. отклонено с ошибкой ОШИБКА_УЖЕ_ОПЛАЧЕНО).
Действия/Выходы: Действия (управление механизмом, индикатором, таймером) и выходные сигналы (ошибки, подтверждения) являются неотъемлемой частью переходов.
Эта модель FSM более абстрактна, но четко описывает логику поведения системы в ответ на внешние события.
Модель: Gemini 2.5 Pro )))
Время: 31.03.2025 18:46:21 GMT+3
Следовало и ждать такого заявления, как в этой статье.
Инкапсуляция очень условное понятие. Принцип кота Шредингера как раз демонстрирует ее, причем не как достижение а как суть проблемы. Или же в более простом варианте как-то ходил снимок какого-то устройства открыв которое стало видно что попросту замкнуты между столбовой 2 разъема и вложена бумажка с ответом вопросом на логично возникающий вопрос.
Тем не мнение и эта абстракция позволяла нам как раз абстрагироваться от сложностей но не гарантировала возможности спрятаться от них. Абстрагируясь от сложностей мы концентрируемся на том что делаем. Например зачем нам знать в каком состоянии находится контролер I²C порта, в моменты когда остальная задача никак не зависит от его текущего состояния, как собственно и на оборот.
Тем не мнение если I²C и остальная задача зависят друг от друга, то им придется взаимодействовать.
Эта статья появилась тогда после активного использования нейронных сетей, программисты вдруг заметили что нейронным сетям плохо даются некоторые вещи как-то объектно-ориентированный анализ.
Объектно-ориентированное программирование не обязательно C++. На чистом C модно получить те же самые абстракции, правда методы придется выделять конвенциально (просто помнить что такие-то функции к относятся к такому-то объекту) а инкапсулированние данные указывать в параметре каждой функции. Правда опять же конвенциально придется не делать с ними то что нельзя. В принципе то же самое делает надстройка над компилятором C для языка ObjectiveC.
Но можно попробовать пойти дальше в языки программирования сверхвысокого уровня, например CIAO этот язык хоть и позволяет много достичь но еще и более требователен в частности к Объектно Ориентированному Анализу, и вероятно поэтому, мало распространен.
Непонятно, как без обработки ошибок реализовать инициализацию типа Temperature из пользовательских данных в не-ООП языках?
Ну вот читаем показания датчика через порт и оттуда внезапно прилетели условные -300 градусов. И что с этим будем делать, в рамках некой идеальной инкапсуляции в не-ООП языках?
Непонятно, с чего вы взяли, что я что-то говорил про некую «идеальную инкапсуляцию в не-ООП языках», это во-первых. Мой тезис звучал так: столпом (одним из самых краеугольных) в ООП является инкапсуляция, которая на деле фактически недостижима.
Я что-то не слышал, чтобы хаскель продавали как идеальную для инкапсуляции парадигму, зато про ООП — это на каждом столбе написано.
Во-вторых, какой-нибудь идрис вас буквально заставит описать тип для температуры, а потом не скомпилирует передачу -300 извне.
В-третьих, обработка ошибок может быть конвенциональной, например, в эрланге процесс упадет и супервизор его перезапустит с последней увиденной температурой, которую он смог обработать, без участия в том программиста.
В-четвертых, в самой обработке ошибок нет ничего плохого, пока мы не заставляем вызывающий код на них как-то реагировать (потому что как именно реагировать — вызывающий код в общем случае не знает).
"я что-то говорил про некую «идеальную инкапсуляцию в не-ООП языках»,
Ну ты так упирал именно на ООП, хотя инкапсуляция возможна не только в ООП.
"зато про ООП — это на каждом столбе написано."
Пример?
"Во-вторых, какой-нибудь идрис вас буквально заставит описать тип для температуры, а потом не скомпилирует передачу -300 извне."
Ты не можешь регулировать то что тебе придёт снаружи. Так что проверки будут, как и обработка ошибочного ввода. Как ты не описывай тип, из порта в/в ты читаешь порток байт которые тебе придётся преобразовать в свой тип. А в случае прихода мусора как-то это обработать и как-то на это отреагировать. И никакой компилятор не гарантирует тебе отсутствие сбоя оборудования.
"В-третьих, обработка ошибок может быть конвенциональной, например, в эрланге процесс упадет и супервизор его перезапустит с последней увиденной температурой, которую он смог обработать, без участия в том программиста."
У тебя датчик навернулся и гонит мусор. Будем на каждый цикл чтения перезапускать процесс?
Впрочем это не важно, у тебя так или иначе будет обработка ошибок.
В любом случае, покажи общепринятое формальное определение инкапсуляции где возврат ошибки и обработка ошибок являются нарушением инкапсуляции?
нам надо бы обработать случай value < 273.15
Тут сразу несколько проблем.
Требования не проанализированы. По всей видимости быстро написали реализацию и в продакшен.
Дизайн не продуман. А надо было бы его нарисовать, выделить интерфейс для работы с температурой, продумать граничные условия, механизм передачи ошибок и т. д. И т. П. .
Если уж требования поменялись, то делайте новую реализацию. Но так как у вас тут все будет зависеть от конкретного класса, а не от интерфейса. То все считайте приехали. Был бы интерфейс можно было бы новую реализацию сделать с проверками, а так все менять придётся.
Если уж вообще требования поменялись, то и вводите новый интерфейс и расширяйте реализацию.
В общем, моё мнение проблема тут в подходе к разработке, сразу код и в продакшен. Так не надо, надо хоть маломальский анализ и Дизайн прикинуть. Иначе код превратиться в лапшу при любой парадигме программирования.
Как изменился бы вывод про нерабочую инкапсуляцию, если бы я замусорил текст двумя страницами корректной имплементации с интерфейсами и дизайном?
Никак, вы бы понимали, что за что отвечает и не трогали бы уже существующие реализии и их использование в легасе коде.
А так и реализацию менять и кучу легаси в 100500 местах.
Аналогия такая. Вы купили чернобелый. телевизор, и вдруг появился цветной. в продаже. Но вместо того, чтобы поменять его на цветной заменой на другой экземпляр, лезите внутрь, и все там сами меняете, схемы, кинескоп итд. Зачем?
ОК. Пусть есть интерфейс, который я опустил, потому что не опасался, будто кто-то не сможет его себе в уме вообразить. ОК.
Есть интерфейс телевизор, реализацию я полностью заменил на цветную. Как это поможет неспособности ООП в инкапсуляцию-то?
В данном случае у вас и проблем не будет, которые вы пытались решать.
У вас инкапсуляция будет скрывать устройство телевизора или получения температуры, как она получается вас не должно интересовать.
Вас должен интересовать контракт. Если в контракте нет возможности передать ошибку, то делайте новый контракт и, например, используя шаблон адаптер реализуйте его
Почему проблема в коде при:
value < 273.15.?
Она должна быть при value < - 273 (0K)
В вашем примере инкапсуляция скрывает как мы храним температуру: как градусы Цельсия или фаренгейта или как нормальные люди как градусы кельвина- кстати единственная реализация которая должна иметь место быть.
Далее если вы пытаетесь задать температуру ниже абсолютного нуля, то тут два возможных варианта:
1 физический - поскольку температура не может быть < 0 К, то можем просто поставить в 0 и забыть. То же самое и с диапазоном:
Либо ставить по нижней/верхней границы и молчать как партизан,
Либо выдать ошибку.
В вашем примере объект temperature - описывает физический объект температуру, которая может быть от 0К до Ххххх сколько-то там.
Она не имеет и не должна проверять диапазоны.
Если вы хотите делать проверки, то вам надо. сделать класс temperatureController (temperatilure min, temperature max)
И функцию установки температуры с ее контролем.
Bool SetTemperatureCelsius( …)
Или вернуть error code,
поскольку температура не может быть < 0 К
Смотря что считать температурой. https://en.wikipedia.org/wiki/Negative_temperature
Спасибо за статью! Восхищен стилем - читается легко и доходчиво, хотя именно за него Вас, наверное, и заминусовали. Нельзя же минусовать за аргументированно высказанную позицию, просто потому, что ты с ней не согласен (кстати, мои плюсы всем, кто аргументированно возражает автору в комментариях!).
Компромиссом являются: unsafe в расте, Any в тайпскрипте, и
всякая прочая потребень
Добавлю до кучи common
в фортране. Формально
это не совсем то
де-факто с помощью common можно локально, внутри объекта, объявить глобальную переменную или структуру, к которой с помощью аналогичного объявления можгно обратиться из внутренностей другого объекта и вообще любой точки программы
но по сути - такой же крах всех надежд ;-)
Спасибо!
Я так давно в последний раз писал на фортране, что, наверное, уже и не вспомню, был тогда common
или еще нет :)
Нельзя же минусовать за аргументированно высказанную позицию, просто потому, что ты с ней не согласен
А за что тогда можно минусовать? Минус это и есть выражение несогласия. На английском это называется еще более явно - "дизлайк", что означает "не нравится". Если я не согласен, значит по этому критерию статья мне не нравится. Не нравится, значит можно поставить дизлайк.
А за что тогда можно минусовать?
Лично я исхожу из того, что минусовать надо заведомо пустые статьи. Бессодержательные, сумбурно написанные, когда автор непонятно чего хочет сказать или просто лыка не вяжет. Рекламу ради рекламы. Явные, но не аргументированные наезды на кого-то или на что-то. Ну и конечно прямой (особенно умышленный) обман и фактологические ошибки, которые могут кого-то ввести в заблуждение и даже подставить. Еще можно минуснуть грубость и неуважение к читателю. Короче говоря, я сам минусую те статьи, которые вообще не стоит читать (и даже открывать не стоит).
Если же автор выдвигает спорный, но по-своему обоснованный тезис, то это называется дискуссия. Жаль, на Хабре такого тега нет. "Мнение" - это все же не совсем то. (Кстати, именно в данном случае тег "Мнение" имхо был бы точнее, чем "кейс").
Минус это и есть выражение несогласия. На английском это называется еще более явно - "дизлайк", что означает "не нравится". Если я не согласен, значит по этому критерию статья мне не нравится. Не нравится, значит можно поставить дизлайк.
Разумеется, такая позиция тоже возможна. Но тогда многие потенциальные читатели, которым есть, что добавить к разговору, могут ее вообще не открыть. А мое скромное имхо состоит в том, что чуть ли не в половине случаев дискуссия в комментах - это столь же ценное дополнение к материалу, как и сама статья. Поэтому я стараюсь возвращаться к особо понравившимся статьям через пару дней, чтобы почитать комментарии.
Ну и третье соображение (к Вам не относится!): часто минус ставят вместо комментария (а не в дополнение к нему). Но если я не дока в этом вопросе (а вот здесь я именно что не дока!), и у меня нет своей твердой позиции, то мне крайне интересны именно возражения и контраргументы. Чтобы увидеть всю палитру мнений с разных сторон, а не только позицию автора. Поэтому я бы очень хотел, чтобы несогласные не минусовали, а отвечали и спорили. Понятно, что не у всех есть время/возможность, но тем не менее.
P.S. Я не пытаюсь Вас переубедить, просто объясняю свою точку зрения.
Лично я исхожу из того
Короче говоря, я сам минусую те статьи
И почему вы решили, что ваше понимание самое верное, и все должны так делать?
просто объясняю свою точку зрения
Нет, вы явно выражаете неодобрение других точек зрения, говорите, что люди не должны так делать, фразой "Нельзя минусовать".
часто минус ставят вместо комментария
Если мою точку зрения уже написал кто-то другой, и я с ней согласен, зачем я должен писать то же самое еще раз? И читать 20 похожих комментариев вместо 20 минусов мне тоже не хочется.
Но тогда многие потенциальные читатели, которым есть, что добавить к разговору, могут ее вообще не открыть.
Если читателям есть что добавить по теме, они не будут смотреть на количество минусов.
А вот если они не разбираются в теме, они будут смотреть на рейтинг, чтобы понять, насколько другие специалисты считают ее правильной и полезной, есть ли смысл учитывать информацию из нее, или лучше прочитать другую статью с более высоким рейтингом. А вы хотите лишить их этой информации и заставить читать десятки однотипных комментариев в надежде найти что-то, что поможет им понять ценность статьи.
Нет, вы явно выражаете неодобрение других точек зрения, говорите, что люди не должны так делать, фразой "Нельзя минусовать".
Да, тут Вы правы. Я хотел выразить свое недоумение, а получилось - неодобрение...
Если мою точку зрения уже написал кто-то другой, и я с ней согласен, зачем я должен писать то же самое еще раз? И читать 20 похожих комментариев вместо 20 минусов мне тоже не хочется.
Согласен, дубли писать - не надо. Можно просто плюсануть комментарий, где мысль хорошо изложена. Тут не поспоришь: к комменту под 20-ю плюсами нельзя не прислушаться ;-)
В остальном же я Вашу точку зрения понял, и рациональное зерно в ней вижу. Но подписаться под ней не готов. Мне все-таки кажется, что те минусы, про которые Вы говорите, больше уместны в комментариях, и в меньшей степени - под статьей.
Э-э-э-э нет.
Во-первых, оратор выше передёргивает и искажает сказанное вами:
вы явно выражаете неодобрение других точек зрения, говорите, что люди не должны так делать, фразой «Нельзя минусовать».
Вы никогда не выражали никакого неодобрения и в принципе не говорили «Нельзя минусовать». Ваша реплика содержала солецизм «Нельзя же минусовать», риторический троп, который семантически вовсе не идентичен «Нельзя минусовать», поэтому казуистическая зевгма вашего оппонента является чистой воды диффамацией.
Во-вторых, ваш оппонент как раз ни к каким апосиопезам не прибегает, и рубит сплеча:
Не нравится, значит можно поставить дизлайк.
Почему-то тут мы, наоборот, сталкиваемся с декларацией «люди должны так делать». Никаких литот, в отличие от вас, заметьте. «Не нравится — стреляй».
Такие дела.
Вы никогда не выражали никакого неодобрения
Выражал. Слово нельзя выражает неодобрение.
"Запрещено, недозволено. Не следует, нехорошо".
"Синонимы: не подобает, не полагается, не рекомендуется, не следует"
Не нравится, значит можно поставить дизлайк.
Почему-то тут мы, наоборот, сталкиваемся с декларацией «люди должны так делать».
Нет, это декларация "люди могут так делать". Направленная на то, чтобы показать, что неодобрение является некорректным, и минусы изначально предназначены для выражения своего отношения к материалу.
"Можно" имеет не тот же смысл, что "должны", можете проверить в словаре.
Вы можете купить Кока-Колу в магазине, но это не значит, что вы должны ее покупать.
Может вы еще буквы будете переставлять в моих словах, чтобы получилось то, что вам нужно, и потом утверждать, что я так сказал?
Может вы еще буквы будете переставлять в моих словах, чтобы получилось то, что вам нужно, и потом утверждать, что я так сказал?
Вообще-то так делаете вы, а я приводил точные цитаты и ничего нигде не переставлял. То, что вы плохо владеете русским языком и не поняли мой комментарий выше — нормально, он и не вам был адресован.
Нет, так делаете вы. Я написал "можно", а вы заменили это на слово "должны" и сделали утверждение, что я так сказал.
То, что вы плохо владеете русским языком
Вам привести ссылки на словарные значения "можно" и "должен"? Я предполагал, что вы достаточно умны, чтобы это было не нужно. Видимо, я ошибся.
можно
"Есть возможность. Разрешается, позволительно."
должен
"Обязан сделать что-н."
Выражение "можно поставить дизлайк" ни в каком смысле не является декларацией "люди должны так делать".
Ну причем тут ООП?
Вот вполне нормальные реализации на ООП
import threading
import time
from enum import Enum, auto
# --- Вспомогательные компоненты (Заглушки) ---
class Mechanism:
def lock(self): print("DEBUG: Mechanism LOCKED")
def unlock(self): print("DEBUG: Mechanism UNLOCKED")
class Timer:
def __init__(self, timeout_seconds, callback):
self._timeout = timeout_seconds
self._callback = callback
self._timer_thread = None
self._lock = threading.Lock()
def start(self):
with self._lock:
self.cancel() # Отменяем предыдущий, если был
print(f"DEBUG: Timer STARTED ({self._timeout}s)")
self._timer_thread = threading.Timer(self._timeout, self._handle_timeout)
self._timer_thread.start()
def cancel(self):
with self._lock:
if self._timer_thread and self._timer_thread.is_alive():
print("DEBUG: Timer CANCELLED")
self._timer_thread.cancel()
self._timer_thread = None
def _handle_timeout(self):
print("DEBUG: Timer FIRED (Timeout)")
self._callback() # Вызываем обработчик таймаута в турникете
class Indicator:
def turn_green(self): print("DEBUG: Indicator GREEN")
def turn_red(self): print("DEBUG: Indicator RED")
class PaymentSystem:
def accept_coin(self): print("DEBUG: Coin ACCEPTED")
def accept_ticket(self): print("DEBUG: Ticket ACCEPTED")
def return_coin(self): print("DEBUG: Coin RETURNED")
# В реальной системе тут была бы валидация, списание и т.д.
# --- Основной класс Турникета ---
class TurnstileState(Enum):
LOCKED = auto()
UNLOCKED = auto()
class TurnstileEventResult(Enum):
SUCCESS = auto()
ERROR_ALREADY_UNLOCKED = auto()
ERROR_ALREADY_LOCKED = auto() # Например, при попытке пройти через запертый
ERROR_INVALID_PAYMENT = auto() # Если бы была валидация
ERROR_TIMEOUT = auto() # Сигнал от таймаута
class Turnstile:
def __init__(self, mechanism: Mechanism, timer: Timer, indicator: Indicator, payment_system: PaymentSystem, timeout_duration=5):
self._state = TurnstileState.LOCKED
self._mechanism = mechanism
self._timer = Timer(timeout_duration, self._handle_timeout) # Таймер вызывает наш внутренний метод
self._indicator = indicator
self._payment_system = payment_system
self._lock = threading.Lock() # Для потокобезопасности
# Начальная настройка
self._mechanism.lock()
self._indicator.turn_red()
def get_state(self):
with self._lock:
return self._state
def insert_coin(self) -> TurnstileEventResult:
with self._lock:
print(f"\nEVENT: COIN received (Current State: {self._state.name})")
if self._state == TurnstileState.UNLOCKED:
print("ACTION: Coin rejected (already unlocked)")
self._payment_system.return_coin()
return TurnstileEventResult.ERROR_ALREADY_UNLOCKED
# Переход LOCKED -> UNLOCKED
self._state = TurnstileState.UNLOCKED
self._payment_system.accept_coin()
self._mechanism.unlock()
self._indicator.turn_green()
self._timer.start()
print(f"STATE CHANGE: -> {self._state.name}")
return TurnstileEventResult.SUCCESS
def insert_ticket(self) -> TurnstileEventResult:
with self._lock:
print(f"\nEVENT: TICKET received (Current State: {self._state.name})")
if self._state == TurnstileState.UNLOCKED:
print("ACTION: Ticket rejected (already unlocked)")
# Обычно билет просто не принимается повторно
return TurnstileEventResult.ERROR_ALREADY_UNLOCKED
# Переход LOCKED -> UNLOCKED
self._state = TurnstileState.UNLOCKED
self._payment_system.accept_ticket() # Предполагаем успешную валидацию/списание
self._mechanism.unlock()
self._indicator.turn_green()
self._timer.start()
print(f"STATE CHANGE: -> {self._state.name}")
return TurnstileEventResult.SUCCESS
def pass_through(self) -> TurnstileEventResult:
with self._lock:
print(f"\nEVENT: PASS detected (Current State: {self._state.name})")
if self._state == TurnstileState.LOCKED:
print("ACTION: Pass attempt blocked (Alarm?)")
# Можно добавить логику тревоги
return TurnstileEventResult.ERROR_ALREADY_LOCKED
# Переход UNLOCKED -> LOCKED
self._state = TurnstileState.LOCKED
self._timer.cancel()
self._mechanism.lock()
self._indicator.turn_red()
print(f"STATE CHANGE: -> {self._state.name}")
return TurnstileEventResult.SUCCESS
def _handle_timeout(self):
# Этот метод вызывается из потока таймера, поэтому нужна блокировка
with self._lock:
print(f"\nEVENT: TIMEOUT detected (Current State: {self._state.name})")
# Таймаут имеет смысл только если мы все еще UNLOCKED
if self._state == TurnstileState.UNLOCKED:
# Переход UNLOCKED -> LOCKED по таймауту
self._state = TurnstileState.LOCKED
self._mechanism.lock()
self._indicator.turn_red()
print(f"STATE CHANGE: -> {self._state.name} (due to timeout)")
# Можно сигнализировать об этом событии вовне, если нужно
# return TurnstileEventResult.ERROR_TIMEOUT # Возвращать некому, т.к. вызвано таймером
else:
# Таймаут пришел, когда уже LOCKED (например, успели пройти) - игнорируем
print("ACTION: Timeout ignored (already locked)")
# --- Пример использования ООП ---
print("--- OOP Turnstile Demo ---")
mech = Mechanism()
indi = Indicator()
pay_sys = PaymentSystem()
# Timer будет создан внутри Turnstile
turnstile_oop = Turnstile(mech, None, indi, pay_sys, timeout_duration=3) # Timer создается внутри
print(f"Initial State: {turnstile_oop.get_state().name}")
result = turnstile_oop.insert_coin()
print(f"Coin Insert Result: {result.name}")
print(f"State after Coin: {turnstile_oop.get_state().name}")
time.sleep(1) # Даем время пройти
result = turnstile_oop.pass_through()
print(f"Pass Result: {result.name}")
print(f"State after Pass: {turnstile_oop.get_state().name}")
print("-" * 20)
result = turnstile_oop.insert_ticket()
print(f"Ticket Insert Result: {result.name}")
print(f"State after Ticket: {turnstile_oop.get_state().name}")
# Пытаемся вставить монету, пока открыто
result = turnstile_oop.insert_coin()
print(f"Second Coin Insert Result: {result.name}")
print(f"State after second Coin: {turnstile_oop.get_state().name}")
print("Waiting for timeout...")
time.sleep(4) # Ждем дольше таймаута
print(f"State after Timeout: {turnstile_oop.get_state().name}")
print("--- OOP Demo End ---")
И ФП:
import time
import threading
from typing import Tuple, List, Any, Dict
# --- Определения состояний, событий, действий (данные) ---
STATE_LOCKED = "LOCKED"
STATE_UNLOCKED = "UNLOCKED"
EVENT_COIN = "COIN"
EVENT_TICKET = "TICKET"
EVENT_PASS = "PASS"
EVENT_TIMEOUT = "TIMEOUT"
EVENT_INVALID = "INVALID" # Пример невалидного входа
EVENT_ATTEMPT_PASS = "ATTEMPT_PASS" # Попытка пройти без оплаты
# Действия - это команды для внешнего исполнителя
ACTION_LOCK_MECHANISM = "LOCK_MECHANISM"
ACTION_UNLOCK_MECHANISM = "UNLOCK_MECHANISM"
ACTION_START_TIMER = "START_TIMER"
ACTION_CANCEL_TIMER = "CANCEL_TIMER"
ACTION_GREEN_LIGHT = "GREEN_LIGHT"
ACTION_RED_LIGHT = "RED_LIGHT"
ACTION_ACCEPT_COIN = "ACCEPT_COIN"
ACTION_RETURN_COIN = "RETURN_COIN"
ACTION_ACCEPT_TICKET = "ACCEPT_TICKET"
ACTION_SIGNAL_ALARM = "SIGNAL_ALARM"
# Результаты для внешнего мира (могут быть частью действий)
SIGNAL_SUCCESS = ("SIGNAL", "SUCCESS")
SIGNAL_ERROR_ALREADY_UNLOCKED = ("SIGNAL", "ERROR_ALREADY_UNLOCKED")
SIGNAL_ERROR_ALREADY_LOCKED = ("SIGNAL", "ERROR_ALREADY_LOCKED")
SIGNAL_ERROR_TIMEOUT = ("SIGNAL", "ERROR_TIMEOUT")
SIGNAL_ERROR_INVALID_PAYMENT = ("SIGNAL", "ERROR_INVALID_PAYMENT")
# --- Чистая функция перехода состояний ---
def handle_event(current_state: str, event: str) -> Tuple[str, List[Any]]:
"""
Чистая функция: принимает состояние и событие, возвращает новое состояние и список действий.
Не выполняет никаких побочных эффектов сама.
"""
if current_state == STATE_LOCKED:
if event == EVENT_COIN:
# Переход LOCKED -> UNLOCKED
new_state = STATE_UNLOCKED
actions = [ACTION_ACCEPT_COIN, ACTION_UNLOCK_MECHANISM, ACTION_GREEN_LIGHT, ACTION_START_TIMER, SIGNAL_SUCCESS]
return new_state, actions
elif event == EVENT_TICKET:
# Переход LOCKED -> UNLOCKED
new_state = STATE_UNLOCKED
actions = [ACTION_ACCEPT_TICKET, ACTION_UNLOCK_MECHANISM, ACTION_GREEN_LIGHT, ACTION_START_TIMER, SIGNAL_SUCCESS]
return new_state, actions
elif event == EVENT_ATTEMPT_PASS:
return STATE_LOCKED, [ACTION_SIGNAL_ALARM, SIGNAL_ERROR_ALREADY_LOCKED]
elif event in [EVENT_PASS, EVENT_TIMEOUT]: # Нерелевантные события
return STATE_LOCKED, []
else: # Неизвестное или невалидное событие
return STATE_LOCKED, [ACTION_RETURN_COIN, SIGNAL_ERROR_INVALID_PAYMENT] # Пример
elif current_state == STATE_UNLOCKED:
if event == EVENT_PASS:
# Переход UNLOCKED -> LOCKED
new_state = STATE_LOCKED
actions = [ACTION_CANCEL_TIMER, ACTION_LOCK_MECHANISM, ACTION_RED_LIGHT, SIGNAL_SUCCESS]
return new_state, actions
elif event == EVENT_TIMEOUT:
# Переход UNLOCKED -> LOCKED
new_state = STATE_LOCKED
actions = [ACTION_LOCK_MECHANISM, ACTION_RED_LIGHT, SIGNAL_ERROR_TIMEOUT] # Таймер сам себя не отменяет
return new_state, actions
elif event == EVENT_COIN:
# Монета во время разблокировки
return STATE_UNLOCKED, [ACTION_RETURN_COIN, SIGNAL_ERROR_ALREADY_UNLOCKED]
elif event == EVENT_TICKET:
# Билет во время разблокировки
return STATE_UNLOCKED, [SIGNAL_ERROR_ALREADY_UNLOCKED] # Билет просто игнорируется
elif event == EVENT_ATTEMPT_PASS: # Попытка пройти вторым - игнор или аларм? Пусть будет аларм.
return STATE_UNLOCKED, [ACTION_SIGNAL_ALARM]
else: # Неизвестное событие
return STATE_UNLOCKED, []
# На случай непредвиденного состояния
return current_state, []
# --- Внешний "Рантайм" или "Исполнитель" ---
# (Упрощенная симуляция, в реальности это был бы цикл обработки событий)
class FunctionalTurnstileRuntime:
def __init__(self, timeout_duration=5):
self._current_state = STATE_LOCKED
self._timeout_duration = timeout_duration
self._timer_handle = None # Храним объект таймера (например, threading.Timer)
self._lock = threading.Lock() # Для атомарности обработки событий
# Заглушки для реальных действий
self._mechanism_state = "LOCKED"
self._indicator_state = "RED"
print(f"FP Runtime Init: State={self._current_state}, Mech={self._mechanism_state}, Indic={self._indicator_state}")
def _execute_actions(self, actions: List[Any]):
""" Выполняет побочные эффекты, управляемые извне """
print(f" Executing Actions: {actions}")
last_signal = None
for action in actions:
if isinstance(action, tuple) and action[0] == "SIGNAL":
print(f" SIGNAL: {action[1]}")
last_signal = action[1] # Запоминаем последний сигнал для возврата
elif action == ACTION_LOCK_MECHANISM:
self._mechanism_state = "LOCKED"; print(" ACTION: Mechanism LOCKED")
elif action == ACTION_UNLOCK_MECHANISM:
self._mechanism_state = "UNLOCKED"; print(" ACTION: Mechanism UNLOCKED")
elif action == ACTION_START_TIMER:
self._cancel_timer_internal() # Отменяем старый, если есть
print(f" ACTION: Timer STARTED ({self._timeout_duration}s)")
self._timer_handle = threading.Timer(self._timeout_duration, lambda: self.process_event(EVENT_TIMEOUT))
self._timer_handle.start()
elif action == ACTION_CANCEL_TIMER:
self._cancel_timer_internal()
elif action == ACTION_GREEN_LIGHT:
self._indicator_state = "GREEN"; print(" ACTION: Indicator GREEN")
elif action == ACTION_RED_LIGHT:
self._indicator_state = "RED"; print(" ACTION: Indicator RED")
elif action == ACTION_ACCEPT_COIN: print(" ACTION: Coin ACCEPTED")
elif action == ACTION_RETURN_COIN: print(" ACTION: Coin RETURNED")
elif action == ACTION_ACCEPT_TICKET: print(" ACTION: Ticket ACCEPTED")
elif action == ACTION_SIGNAL_ALARM: print(" ACTION: ALARM!")
# ... другие действия ...
return last_signal # Возвращаем последний сигнал как результат операции
def _cancel_timer_internal(self):
if self._timer_handle and self._timer_handle.is_alive():
print(" ACTION: Timer CANCELLED")
self._timer_handle.cancel()
self._timer_handle = None
def process_event(self, event: str) -> Any:
"""
Главный метод обработки события:
1. Блокировка для атомарности.
2. Вызов чистой функции handle_event.
3. Обновление внутреннего состояния.
4. Разблокировка.
5. Выполнение действий (побочных эффектов).
"""
print(f"\nFP EVENT: {event} (Current State: {self._current_state})")
with self._lock:
new_state, actions = handle_event(self._current_state, event)
if new_state != self._current_state:
print(f" STATE CHANGE: {self._current_state} -> {new_state}")
self._current_state = new_state
else:
print(f" State unchanged: {self._current_state}")
# Копируем actions, чтобы выполнить их вне блокировки (если нужно)
actions_to_execute = list(actions)
# Выполнение действий происходит после освобождения блокировки состояния
# (Хотя запуск/остановка таймера тоже требуют синхронизации, как в _execute_actions)
result_signal = self._execute_actions(actions_to_execute)
return result_signal # Возвращаем сигнал от выполнения действий
def get_current_state_info(self): # Для отладки
return f"FP State={self._current_state}, Mech={self._mechanism_state}, Indic={self._indicator_state}"
# --- Пример использования ФП ---
print("\n--- FP Turnstile Demo ---")
turnstile_fp = FunctionalTurnstileRuntime(timeout_duration=3)
print(f"Initial: {turnstile_fp.get_current_state_info()}")
result = turnstile_fp.process_event(EVENT_COIN)
print(f"Coin Insert Result Signal: {result}")
print(f"State after Coin: {turnstile_fp.get_current_state_info()}")
time.sleep(1)
result = turnstile_fp.process_event(EVENT_PASS)
print(f"Pass Result Signal: {result}")
print(f"State after Pass: {turnstile_fp.get_current_state_info()}")
print("-" * 20)
result = turnstile_fp.process_event(EVENT_TICKET)
print(f"Ticket Insert Result Signal: {result}")
print(f"State after Ticket: {turnstile_fp.get_current_state_info()}")
# Пытаемся вставить монету, пока открыто
result = turnstile_fp.process_event(EVENT_COIN)
print(f"Second Coin Insert Result Signal: {result}")
print(f"State after second Coin: {turnstile_fp.get_current_state_info()}")
print("Waiting for timeout...")
time.sleep(4) # Ждем обработку TIMEOUT внутри runtime
print(f"State after Timeout: {turnstile_fp.get_current_state_info()}")
print("--- FP Demo End ---")
Или даже имитация зависимых состояний
// 1. Типы-маркеры для состояний
struct Locked;
struct Unlocked;
// 2. Основная структура, параметризованная состоянием
struct Turnstile<State> {
// Общие данные турникета, если есть
id: String,
// Важно: State - это тип-маркер, он не занимает места в памяти
_state: std::marker::PhantomData<State>,
}
// 3. Реализация методов для СОСТОЯНИЯ LOCKED
impl Turnstile<Locked> {
pub fn new(id: String) -> Self {
println!("{} is LOCKED", id);
// ... инициализация механизма ...
Turnstile { id, _state: std::marker::PhantomData }
}
// Этот метод доступен ТОЛЬКО для Turnstile<Locked>
// Он "потребляет" self и возвращает новый турникет в состоянии Unlocked
pub fn insert_coin(self) -> Turnstile<Unlocked> {
println!("{} Coin accepted, UNLOCKING", self.id);
// ... разблокировать механизм, запустить таймер ...
Turnstile { id: self.id, _state: std::marker::PhantomData }
}
pub fn insert_ticket(self) -> Turnstile<Unlocked> {
println!("{} Ticket accepted, UNLOCKING", self.id);
// ... разблокировать механизм, запустить таймер ...
Turnstile { id: self.id, _state: std::marker::PhantomData }
}
// Попытка вызвать pass() на Turnstile<Locked> просто не скомпилируется!
// pub fn pass(self) -> ??? { ... } // Ошибка! Нет такого метода для Locked.
}
// 4. Реализация методов для СОСТОЯНИЯ UNLOCKED
impl Turnstile<Unlocked> {
// Этот метод доступен ТОЛЬКО для Turnstile<Unlocked>
pub fn pass(self) -> Turnstile<Locked> {
println!("{} Passed, LOCKING", self.id);
// ... остановить таймер, заблокировать механизм ...
Turnstile { id: self.id, _state: std::marker::PhantomData }
}
pub fn timeout(self) -> Turnstile<Locked> {
println!("{} Timeout, LOCKING", self.id);
// ... заблокировать механизм ...
Turnstile { id: self.id, _state: std::marker::PhantomData }
}
// Попытка вызвать insert_coin() на Turnstile<Unlocked> не скомпилируется!
}
// --- Использование ---
fn main() {
let t1 = Turnstile::<Locked>::new("T1".to_string());
// Это компилируется: insert_coin() определен для Turnstile<Locked>
let t2: Turnstile<Unlocked> = t1.insert_coin();
// Это компилируется: pass() определен для Turnstile<Unlocked>
let t3: Turnstile<Locked> = t2.pass();
// А вот это НЕ СКОМПИЛИРУЕТСЯ:
// let t4 = t3.pass();
// error[E0599]: no method named `pass` found for struct `Turnstile<Locked>` in the current scope
// --> src/main.rs:67:16
// |
// 67 | let t4 = t3.pass();
// | ^^^^ method not found in `Turnstile<Locked>`
// И это НЕ СКОМПИЛИРУЕТСЯ:
// let t2_again = Turnstile::<Unlocked>::new("T2".to_string()); // Нет такого конструктора
// let t5 = t3.insert_coin();
// error[E0599]: no method named `insert_coin` found for struct `Turnstile<Locked>`
println!("Final state type check successful (compilation passed).");
}
О! болтун подкинул прикольную идею по реализациям конечного автомата
приведу текстом, а не ссылкой
Открыть в браузере Скопировать ссылку
Это очень интересное и довольно меткое наблюдение! Действительно, переход от enum/match к typestate можно рассматривать как своего рода "транспонирование" взгляда на таблицу переходов состояний.
Давайте представим таблицу переходов состояний как матрицу:

(Где A1, A2... - это наборы действий)
1. Подход enum + match (Классический/По Событиям):
Организация кода: Вы пишете функции/методы для каждого СТОЛБЦА (События).
fn handle_coin(&mut self)
fn handle_pass(&mut self)
fn handle_timeout(&mut self)
Внутри каждой функции: Вы используете match self.state { ... } для выбора СТРОКИ (Текущего Состояния) и выполнения соответствующей логики из ячейки [Текущее Состояние][Событие].
Аналогия: Вы "читаете" матрицу по столбцам. Для каждого события вы смотрите, в каком вы сейчас состоянии, чтобы понять, что делать.
// Читаем столбец "Coin"
fn handle_coin(&mut self) {
match self.state {
Locked => { /* Ячейка [Locked][Coin] */ }
Unlocked => { /* Ячейка [Unlocked][Coin] */ }
}
}
// Читаем столбец "Pass"
fn handle_pass(&mut self) {
match self.state {
Locked => { /* Ячейка [Locked][Pass] */ }
Unlocked => { /* Ячейка [Unlocked][Pass] */ }
}
}
2. Подход typestate (Состояние в Типе):
Организация кода: Вы пишете блоки impl для каждой СТРОКИ (Состояния).
impl Turnstile<Locked> { ... }
impl Turnstile<Unlocked> { ... }
Внутри каждого impl: Вы определяете методы для валидных СТОЛБЦОВ (Событий), которые могут произойти из этого состояния. Логика метода соответствует ячейке [Данное Состояние][Событие]. Методы для невалидных событий просто не существуют для данного типа.
Аналогия: Вы "читаете" матрицу по строкам. Для каждого состояния вы заранее определяете, какие события возможны и что они делают.
// Читаем строку "Locked"
impl Turnstile<Locked> {
// Ячейка [Locked][Coin]
fn insert_coin(self) -> Turnstile<Unlocked> { /* ... */ }
// Ячейка [Locked][Pass] - можно определить метод для обработки ошибки (alarm)
// fn pass(self) -> Self { /* Alarm */ }
// ИЛИ просто не определять, тогда вызов будет ошибкой компиляции.
// Ячейка [Locked][Timeout] - скорее всего, не нужен, т.к. таймер не запущен
}
// Читаем строку "Unlocked"
impl Turnstile<Unlocked> {
// Ячейка [Unlocked][Coin] - можно определить метод для обработки ошибки (reject)
// fn insert_coin(self) -> Self { /* Reject Coin */ }
// Ячейка [Unlocked][Pass]
fn pass(self) -> Turnstile<Locked> { /* ... */ }
// Ячейка [Unlocked][Timeout]
fn timeout(self) -> Turnstile<Locked> { /* ... */ }
}
Так в чем "транспонирование"?
Вы меняете основную ось, по которой организуете код, отвечающий за переходы:
enum/match: Ось = Событие. Внутри события разбираетесь с состояниями.
typestate: Ось = Состояние. Внутри состояния разбираетесь с применимыми событиями.
Почему это "лучше" (в контексте Rust и безопасности):
Хотя это похоже на транспонирование с точки зрения организации логики, typestate делает нечто большее: он переносит проверку "а можно ли вообще применить это событие к этому состоянию?" с runtime на compile-time.
В enum/match вы можете вызвать handle_pass() на заблокированном турникете. Код скомпилируется, но внутри match вам нужно будет обработать эту ситуацию (например, Locked => { /* Alarm! */ }). Вы можете забыть это сделать или сделать неправильно.
В typestate вы не можете вызвать t_locked.pass(), если метод pass не определен в impl Turnstile<Locked>. Код просто не скомпилируется. Компилятор гарантирует, что вы вызываете только те "события" (методы), которые допустимы для текущей "строки" (типа состояния).
Так что да, аналогия с транспонированием отлично схватывает изменение перспективы и организации кода. Но ключевое преимущество typestate лежит в том, что эта "транспонированная" структура позволяет компилятору статически проверять корректность доступов к "ячейкам" этой воображаемой матрицы.
В целом, если безопасность и надежность в приоритете (а в Rust это часто так), typestate является более мощным и предпочтительным подходом, особенно когда сложность FSM растет (в основном за счет числа состояний).

Модель: Gemini 2.5 Pro
Время: 31.03.2025 19:23:08 GMT+3
Имхо тут вопрос в том, чего больше. Состояний или событий.
Событий всегда больше (в вырожденном случае детерминированного автомата — столько же).
Ну почему же?
1000 состояний.
Нажатие на оду кнопку - переход в следющуее состояние.
Нажатие на другую кнопку - активация какого-то эффекта, в зависисмости от текущего сосотяния.
Два события, 1000 состояний.
Описывать все это в typestate?
encapsulation prevents external code from being concerned with the internal workings of an object
Инкапсуляция должна бы запретить любое изменение внутреннего состояния сущности (объекта) извне любым способом, кроме специально разрешенных автором.
Почему у вас "prevents external code from being concerned" вдруг превратилось в "forbids external code to be able to change"? "Сoncerned" и "change" это же не синонимы. Если вызывающий код не имеет доступа к private-полям, то он уже "prevented", потому что вы можете переименовать поля эти поля и поменять их типы, не меняя вызывающий код.
Что значит «превратилось»?
Как вам вообще могло прийти в голову, что я стану переводить какого-то дурачка, что-то там написавшего в википедии?
Если вызывающий код не имеет доступа к private-полям, то он уже «prevented», потому что вы можете переименовать поля эти поля и поменять их типы, не меняя вызывающий код.
Конечно, конечно.
Как вам вообще могло прийти в голову, что я стану переводить какого-то дурачка
Ну вы же спорите со значением термина, который придуман каким-то дурачком.
Какой источник вы привели в статье, про тот я и говорю. Если вы считаете, что в Википедии написано неправильно, непонятно, зачем вы тогда его приводили. Вы можете привести тот источник, определение в котором соответствует вашему пониманию?
Конечно, конечно.
Это общепринятое значение термина "инкапсуляция". Вы конечно можете понимать его по-своему, но тогда и все ваши доказательства относятся только в вашему пониманию. Вы придумали свое значение этого термина и доказали, что ООП ему не соответствует. Ну ок, только к исходному значению это никакого отношения не имеет.
кишки внутреннего устройства класса Temperature теперь торчат наружу.
Снаружи придется либо поймать исключение, либо обработать код возврата.
Если исключение или код возврата описаны в сигнатуре, то это ничем не отличается от типа возвращаемого значения, и является частью протокола взаимодействия, а не деталями реализации.
Что-то предпринять, определенно, надо
Но что именно — мы не знаем, потому что (та-дам!) инкапсуляция.
В вызывающем коде знаем. Что именно надо предпринять, зависит от вызывающего кода, а не от вызываемого.
Это как если бы начальник вас спросил "Слетай на Луну, принеси мне лунного грунта - Это не входит в мои рабочие обязанности - Что мне делать с этим ответом?". Вы бы сказали "Что хотите, то и делайте, я же не знаю нафига вам лунный грунт". А если бы не сказали, то подумали.
код не отработал, как было задумано
Если в коде явно написано return errorCode
, значит это было задумано. То есть была предусмотрена эта ситуация и написана обработка для нее.
Там может быть и return Enum.ValueA; ... return Enum.ValueB; ...
на 10 значений. Какое из них не было задумано?
практически невозможно имплементировать на объектной модели, не потеряв математическое доказательство консистентности
А разве объектная модель утверждает, что она гарантирует математическое доказательство консистентности?
Это просто описание, чтобы программисту было проще работать с кодом.
Если исключение или код возврата описаны в сигнатуре
Ну так ведь суть в том, что обычно нигде в сигнатуре не помечаются все исключения, выбрасываемые функцией. По причине того, что это слишком сильно фиксирует библиотеки.
Подойдите формально: получается, что мы взяли функцию температуры, вызываем её уже лет 5, потом автор решил добавить исключение, про которое мы ни сном ни духом, и когда пользователь вводит температуру -1 вся программа падает без сохранения данных по Unhandled Exception.
обычно нигде в сигнатуре не помечаются все исключения, выбрасываемые функцией
Это проблема не ООП как методологии а отдельных его реализаций.
Типы исключений существуют отдельно и не включены в класс. Это иногда плохо. Но он может наследовать свои исключения от существующих
Вы все время делаете ключевую ошибку: вы обсуждаете идеальный мир, в котором реализации выверенные, а разработчики не допускают ошибок.
Мир разработки ПО не такой.
Это проблема не ООП как методологии а отдельных его реализаций.
Ну не помечают исключения в сигнатурах не потому, что не хотят, а потому, что не могут. Это просто приведёт к тому, что в стандартной библиотеке и других станет практически невозможно изменить набор исключений.
Ну так ведь суть в том, что обычно нигде в сигнатуре не помечаются все исключения
Конкретно тут суть не в этом, потому что автор упомянул их вместе с кодом ошибки, который в сигнатуре помечается.
потом автор решил добавить исключение, про которое мы ни сном ни духом
А при чем тут ООП? Так можно сделать в любом языке.
Ловить исключения по типу это вообще неправильно. Надо ловить по общему классу, делать откат нужных действий, и бросать дальше. Поэтому вам и не надо знать, какое там исключение.
Вдруг он внутри вызывает сетевой сервис, и сеть может быть недоступна, даже если он не добавлял новых исключений. Вдруг там где-то ненамеренная ошибка в коде, и при определенных условиях возникает NullPointerException.
В этом смысл абстракции, вы не знаете деталей реализации, поэтому надо учитывать, что всегда может быть RuntimeException, и думать, нужно ли его обрабатывать в вашем коде.
Ловить исключения по типу это вообще неправильно. Надо ловить по общему классу, делать откат нужных действий, и бросать дальше.
Пусть правильно, но ведь checked exceptions вводили и так они и не введены везде. По вполне понятной причине — это слишком жёстко фиксирует потроха библиотек.
Да, я сейчас размахиваю руками, но есть такое, что вот эти высокие древовидные иерархии ООП работают так себе. А классы типов Хаскеля эту проблему решают, позволяя делать иерархии отдельных свойств.
Классы типов Хаскеля эту проблему, безусловно, решают. Но мама дорогая, сколько же лишних они приносят :)
Я пришел (очень тернистым путём) к заключению, что хранить данные нужно одним из следующих трех способов: список, кортеж/массив, мап. Все остальное — избыточная когнитивная нагрузка ради ничего. Джо Армстронг и тут оказался прав.
эти высокие древовидные иерархии ООП работают так себе
А при чем тут древовидные иерархии? Разговор про инкапсуляцию в одном классе.
А разве объектная модель утверждает, что она гарантирует математическое доказательство консистентности?
Нет, именно что нет, про это написан текст выше, и на основании именно этого я заявляю, что объектная модель — мешает писать хороший код, пряча за нагромождением бессмысленных наукообразных абстракций собственную импотенцию в помощи разработчику.
именно что нет
Ну тогда какие претензии? Объектная модель не обещала вам математической консистентности. Если она вам нужна, используйте совместно с объектной моделью тот инструмент, который эту консистентность обеспечивает. А у вас получается "Молоток мешает делать хорошую работу, потому что не умеет откручивать шурупы".
математическое доказательство консистентности
мешает писать хороший код
Вы опять подменяете понятия. Придумали свое значение термина "хороший код", и пытаетесь что-то доказать. Вашему пониманию хорошего кода ООП может и не соответствует, но у других людей другое понимание этого термина.
Мне никогда не было нужно доказательство математической консистентности. В физическом мире ее вообще нельзя гарантировать. Для меня хороший код это в первую очередь понятный код.
Есть книга под названием "Совершенный код", много разработчиков считают ее хорошей. Там тоже нет ничего про доказательство математической консистентности. Зато есть про понятность и выразительность.
Есть только два варианта разрешения конфликта: 1 блокировать монетоприёмник и считыватель карт пока турникет открыт,
1 нарушает инкапсуляцию (потроха нашего FSM экспортируются в прошивку железяк самого турникета)
Если у вас FSM только считает состояние и больше ничего не делает, то очевидно, что кто-то снаружи должен это состояние прочитать, чтобы что-то сделать.
Если он должен что-то еще делать, то вы передаете эту зависимость в конструктор, и FSM сам ее вызывает при переходе в нужное состояние.
или 2 накапливать «проходы» и не закрывать турникет
2 невозможен в ООП без плясок с бубном
Очередь и FSM это 2 разных компонента. Если вы хотите накапливать события, очередь вам нужна будет хоть с ООП, хоть не с ООП. Если не хотите, не нужна. ООП тут ни при чем, как вам надо по задаче, так и делаете. Непонятно, почему вы ждете, что FSM в стиле ООП будет вам магически сам очередь обрабатывать.
Если вы не хотите "закрывать и тут же снова открывать", то область действия FSM выходит за рамки процедуры для одного человека. В алгоритме перехода состояний он должен учитывать, есть ли следующий человек в очереди. В событии "человек прошел" будет флаг "есть следующий человек", от которого зависит, переходить ли в состояние "дверь закрыта". Это идет из бизнес-требований, а не из инкапсуляции.
В общем, инкапсуляция — это сокрытие деталей реализации от внешнего мира, которому доступны только те возможности работы с инкапсулируемой сущностью, которые посчитал правильными экспортировать автор этого черного ящика.
Это называется "абстракция".
Инкапсуляция должна бы запретить любое изменение внутреннего состояния сущности (объекта) извне любым способом, кроме специально разрешенных автором.
А это называется "сокрытие".
Корректное же определение инкапсуляции у Вики было до её изнасилования в 2022:
размещение в одном компоненте данных и методов, которые с ними работают
Иронично, с каким комментарием была внесена та правка:
исправил неверное определение инкапсуляции - было определено очень узко и не полно
---
кишки внутреннего устройства класса Temperature теперь торчат наружу. Снаружи придется либо поймать исключение
Это не кишки внутреннего устройства, а предусловие публичного контракта, которое было нарушено вашим сломанным датчиком, что разумно привело к быстрому падению.
В стейте хранится остаток уже оплаченных проходов, и при попадании в состояние «закрыто» при положительном балансе — турникет открывается сам собой.
Как он у вас попадёт в состояние "закрыто" без сброса баланса ума не приложу. У вас инвариант сломался.
Корректное же определение инкапсуляции у Вики было до её изнасилования в 2022:
размещение в одном компоненте данных и методов, которые с ними работают
Тоже не корректное
Инкапсуляции это представление простых способов взаимодействия с объектом и ограничение возможности изменения объекта.
Но представить на чтение всего содержания произвольным способом не нарушает инкапсуляцию.
Как он у вас попадёт в состояние «закрыто» без сброса баланса ума не приложу.
Это напрямую зависит от реализации автомата. Можно сделать условный переход из «открыто» в любое состояние в зависимости от баланса, а можно безусловный из «открыто» всегда в «закрыто», а из «закрыто» — сразу после этого — безусловный в «открыто», при положительном балансе. Второй вариант предпочтительнее, по очень многим причинам.
А еще программисты допускают ошибки иногда, представляете? Полагаться на отсутствие человеческих ошибок в коде может только очень недалёкий разработчик.
Не стоит человека пугать закрывающимся перед носом турникетом только потому, что пытаетесь совладать со сложностью путём редуцирования числа переходов конечного автомата. Освойте лучше реактивное программирование и поймёте как писать простой и надёжный код без комбинаторного взрыва переходов даже не будучи далёким разработчиком.
Давайте заключим договор: вы перестанете множить кросс-референсы на ахинею, а я перестану вам говорить, куда вам имеет смысл с вашими идиотскими советами пройти.
Win-win.
Для начала надо хотя бы описать эти состояния конечного автомата а не вытаскивать новые как зайца из шляпы
Конечный автомат (некоторые его реализации) на это плохо реагирует
Инкапсуляция использует и включает сокрытие. Абстракция использует (реализована в том числе через) инкапсуляцию
Да нет же, инкапсуляция вообще не про это. Она про реализацию полиморфизма, посредством диспетчеризации функций через объект. Грубо говоря, вместе с данными вам приходят также и ссылки на функции для работы с этими данными. Это мало того что избавляет от необходимости эти функции откуда-то доставать, так ещё и позволяет работать с разными, даже заранее не известными, структурами.
Инкапсуляция и полиморфизмом подтипов работают через интерфейсы (контракты), но цели разные.
И зачем вы опять говорите только про ООП? Сокрытие можно реализовать через модули или через замыкание функции
Как-то излишне драматизированно. Смысл инкапсуляции то довольно простой - уменьшение сложности за счет выделения интерфейса, в рамках использования которого объект остается в корректном состоянии. Когда вместо километров видимых переменных и их торчащих кишок можно думать и оперировать в рамках отдельных объектов-кирпичиков, которых и самих в области видимости меньше, и осмысленных действий над ними. Строго говоря, для этого даже не требуется сокрытие, достаточно просто соглашений или аннотаций. Если программист понимает вышеизложенный принцип и знает зачем ему следовать, то проблем у него не возникнет.
А в описанном примере с температурой проблемы с инкапсуляцией нет. В нем демонстрируется банальная ограниченность языка в возможности точно выразить контракт в интерфейсе сущности. То что температура не может быть ниже указанной просто будет записано в документации, как и поведение в обратном случае.
Тут даже объект не нужен, пример можно переписать как чистую функцию которая принимает температуру и считает что-то по формуле, при этом возникнет необходимость в аналогичной проверке и реакции. Не жаловаться же тогда на реализацию абстракции функциями?
Чтение статьи натолкнуло на следующее воспоминание. Люди старшего поколения рассказывали, что академик Никита Моисеев на лекциях в МФТИ говорил, что статистическая теория турбулентности пример того, когда очень хорошее знание математики не всегда приводит к хорошему результату.
Если на Силикатной улице (это в Мытищах) остановить тысячу случайных прохожих ...
Да-да, «В час жаркого весеннего заката на Патриарших прудах появилось двое граждан...».... Автор немного не добежал до Первого апреля. ;-)
Ох, уж, эти разговоры вокруг терминов!
«Незнакомец всплеснул руками. — Какие пустяки! — сказал он. — Как много шума из-за какого-то, простите, вздора, в который никто к тому же по-настоящему не верит… Посудите сами, Александр Иванович, устраивать склоки, безобразные кинопогони, беспокоить людей из-за мифического — я не боюсь этого слова — именно мифического Белого Тезиса… Каждый трезво мыслящий человек рассматривает диван как универсальный транслятор, несколько громоздкий, но весьма добротный и устойчивый в работе. И тем более смешны старые невежды, болтающие о Белом Тезисе… Нет, я и говорить не желаю об этом диване.» — Стругацкие, «Понедельник начинается в субботу».
Поговорим, немного, о диване. Или О белом тезисе.
По моим устаревшим сведениям, инкапусляция — это попытка поместить некоторые данные и код в некую закрытую сущность (объект и/или класс) так, чтобы снаружи это всё можно было «дёргать» только через заданный интерфейс. Здесь ничего не говорится о том, что создаваемая сущность никак не зависит от других классов, или другие классы никак не зависят от данной сущности.
Посмотрите на Ваш пример с температурой. Вы используете в качестве реализации Double. Фактически, Вы задаёте некий домен (это всё восходит ещё стародавним временам, когда оформлялись технологии баз данных, и, в частности, оформлялась реляционная теория), то есть — набор или диапазон допустимых значений и оформляете этот объект как новую сущность. Это означает, что Вам нужно всюду, где встречается температура, использовать эту сущность. Вы же пишете:
Double temp = getTempFromSensor(); // from outside
Зачем это нужно? Можно было бы ожидать что-то вроде этого:
Temperature temp = sensor->value(); // from outside
Сенсор может ничего не знать про какую-то там температуру, его задача — сформировать некое текущее значение и отдать его пользователю. В то же время, класс Температура знает, что делать с этим значением.
public class Temperature {
class const minValue=-273.15;
Bool valid;
Double celcius;
public Double getCelsius() {
return celsius;
}
public void setCelsius(Double value) {
if (value<minValue)
valid=false;
else {
celsius = value;
valid=true;
}
}
}
Сенсор может отдать только double
, на то он и бездушная железяка.
У вас помле неудачного вызова setCelsius
всё еще можно вызвать getCelsius
и получить устаревшее, неверное значение обратно. Вся инкапсуляция рассыпалась.
use thiserror::Error;
#[derive(Error, Debug, Clone, PartialEq)]
pub enum TemperatureError {
#[error("Temperature value {0} is below minimum {1}")]
BelowMinimum(f64, f64),
#[error("Temperature value {0} is above maximum {1}")]
AboveMaximum(f64, f64),
#[error("Current temperature value is invalid")]
InvalidCurrentValue,
#[error("Invalid range: minimum {0} is greater than maximum {1}")]
InvalidRange(f64, f64),
}
pub struct Temperature {
value: f64,
min_value: f64,
max_value: f64,
is_valid: bool,
}
impl Temperature {
pub fn new(value: f64, min_value: f64, max_value: f64) -> Result<Self, TemperatureError> {
if min_value > max_value {
return Err(TemperatureError::InvalidRange(min_value, max_value));
}
if value < min_value {
return Err(TemperatureError::BelowMinimum(value, min_value));
}
if value > max_value {
return Err(TemperatureError::AboveMaximum(value, max_value));
}
Ok(Self {
value,
min_value,
max_value,
is_valid: true,
})
}
pub fn new_unchecked(value: f64, min_value: f64, max_value: f64) -> Self {
let is_valid = min_value <= value && value <= max_value && min_value <= max_value;
Self {
value,
min_value,
max_value,
is_valid,
}
}
pub fn value(&self) -> Result<f64, TemperatureError> {
if !self.is_valid {
return Err(TemperatureError::InvalidCurrentValue);
}
Ok(self.value)
}
pub fn value_unchecked(&self) -> f64 {
self.value
}
pub fn min_value(&self) -> f64 {
self.min_value
}
pub fn max_value(&self) -> f64 {
self.max_value
}
pub fn is_valid(&self) -> bool {
self.is_valid
}
pub fn validate(&mut self) -> Result<(), TemperatureError> {
if self.min_value > self.max_value {
self.is_valid = false;
return Err(TemperatureError::InvalidRange(self.min_value, self.max_value));
}
if self.value < self.min_value {
self.is_valid = false;
return Err(TemperatureError::BelowMinimum(self.value, self.min_value));
}
if self.value > self.max_value {
self.is_valid = false;
return Err(TemperatureError::AboveMaximum(self.value, self.max_value));
}
self.is_valid = true;
Ok(())
}
pub fn set_value(&mut self, new_value: f64) -> Result<(), TemperatureError> {
self.value = new_value;
// Возвращает ошибку и делает текущее значение невалидным
// при выходе зе пределы диапазона
self.validate()
}
pub fn increase(&mut self, amount: f64) -> Result<(), TemperatureError> {
if !self.is_valid {
return Err(TemperatureError::InvalidCurrentValue);
}
self.set_value(self.value + amount)
}
pub fn decrease(&mut self, amount: f64) -> Result<(), TemperatureError> {
if !self.is_valid {
return Err(TemperatureError::InvalidCurrentValue);
}
self.set_value(self.value - amount)
}
}
Инкапсуляция должна бы запретить любое изменение внутреннего состояния сущности (объекта) извне любым способом, кроме специально разрешенных автором. Вот классический вариант:
public class Temperature {
Double celcius;
public Double getCelsius() {
return celsius;
}
public void setCelsius(Double value) {
celsius = value;
}
}
А откуда вы взяли этот классический пример? Зачем температура сделана мутабельной? Получается, что например 1°C как сущность может стать внезапно 2°C. Это физически не имеет смысла.
Вроде бы во многих статьях об ООП не раз говорилось, что добавление геттеров\сеттеров не приводит к инкапсуляции автоматически. Пример выше ничем не отличается от класса с публичным полем celsius.
Логичнее сделать так:
public class Temperature {
double celsius;
public double getCelsius() {
return celsius;
}
public double getKelvin() {
return celsius + 273.15;
}
public static fromCelsius(double value) Temperature {
if (value < −273.15) {
throw new InvalidTemperature()
}
return new Temperature(value)
}
private Temperature(double celsius) {
this.celsius = celsius
}
}
double value = getTempFromSensor(); // from outside
try {
Temperature t = Temperature.fromCelsius(value)
System.out.println(t.getCelsius())
} catch(InvalidTemperature e) {
System.out.println("Sensor is broken! Call a technician.");
}
Здесь сокрыт способ хранения температуры. Можно хранить в кельвинах. Можно не double, а string. Как хотите.
Если играться в ООП по-серьёзному, то надо типа вот так:
Sensor sensor = new RealSensor();
Temperature t = Temperature.fromSensor(sensor);
А все эти double, Kelvin и Celsius - это и есть "кишки наружу") , поди
Все так. Но где-то в конце тоннеля, или, как говорят наши коллеги из-за океана, all turtles down, все равно будут числа (как и во взрослом ФП в конце всегда будет unpure, а в расте — unsafe). Увы.
В Советской России Температура измеряет Сенсоры.
Так не надо. Не надо давать температуре ответственность считывать датчик. Это не выглядит логичным.
Что касается Kelvin и Celsius - если не подходит такой простенький вариант температуры, то нужно ввести понятие TemperatureScale, которое будет знать о правилах конвертации температур в разных шкалах. Можно захардкодить в один класс, можно делать отдельные KelvinScale, CelsiusScale.
В Temperature будет метод from(value, scale). И все занимаются тем, чем должны - температура представляет физическое понятие, шкала делает конвертацию.
Так не надо. Не надо давать температуре ответственность считывать датчик. Это не выглядит логичным.
Ок. Это делает температуру слишком "знающим" объектом. Это мог бы делать объект типа DispatcherTemperature, а Temperature лучше иметь простым "плоским" обьектом.
Sensor sensor = new RealSensor();
DispatcherTemperature dispatcher = DispatcherTemperature.fromSensor(sensor);
Temperature t = sensor.getTemperature();
Но идея остаётся всё той же, спрятать всё что возможно, все "кишки" Temperature от как можно большего взаимодействия с ними других объектов. Ибо как только вы их "показали наружу", то при изменении имплементации объекта Temperature, вам придётся это учитывать.
Да, как пишет выше @cupraer- "Но где-то в конце тоннеля, или, как говорят наши коллеги из-за океана, all turtles down, все равно будут числа (как и во взрослом ФП в конце всегда будет unpure, а в расте — unsafe). " - Но это вовсе не означает что мы можем использовать в ФП unpure ("нечистые") функции где нам вздумается.
Аналогично и с инкапсуляцией, поди.
Бизнес-объект "датчик" и бизнес-объект "температура" представляющий собой коллекцию объектов, в которой каждый объект содержит в себе данные об идентификаторе датчика, временном моменте измерения и значение температуры в этот момент.
Если измеренная температура ниже -273.15°C, то объект-датчик не вносит её в бизнес-объект "температура", а в лог приложения или в специальный объект, собирающий информацию об ошибках измерений, вносит данные о возникшей ошибке измерения температуры.
А бизнес-объект температура продолжает показывать «–30°C», как оно было установлено в последний раз до поломки датчика, два месяца назад?
Не совсем понял Ваш вопрос. Бизнес-объект температура содержит данные которые измерил датчик. Если датчик определил, что температура физически невозможна, то он не вносит новое значение в коллекцию объектов, которая находится внутри бизнес-объекта температура.
Ну вот датчик намерил у меня на балконе –30°C два месяца назад, а потом поломался, и с тех самых пор обновления никуда не вносит.
Поэтому на табло у меня в гостиной видно старое значение, последнее внесенное.
Хорошее замечание. Но оно относится не к тому функционалу который изменяет, а к тому который отображает. Отображающий функционал должен учитывать разность во времени между текущим временем и временем последнего измерения. Если эта разность во времени превышает определённую дельту, то индикатор вместо температуры отображает "UNDEFINED".
О, бизнес-объект температура теперь хранит время последнего измерения? Мило.
В моём 1м комменте написано - бизнес-объект "температура" представляющий собой коллекцию объектов, в которой каждый объект содержит в себе данные об идентификаторе датчика, временном моменте измерения и значение температуры в этот момент. Бизнес-объект "температура" хранит всё измеренные значения или N последних измеренных значений.
Это мог бы делать объект типа DispatcherTemperature, а Temperature лучше иметь простым "плоским" обьектом.
Да, как я и написал во втором варианте, температура просто представляет физическое понятие, а логика конвертации уезжает в шкалы.
Я так понимаю, вы в последней строке имеете ввиду dispatcher, а не sensor?
Temperature t = dispatcher.getTemperature();
Собственно, да. Кто-то же должен организовать работу. В нем и делать опрос датчика и конвертацию температуры. Конвертацию можно впихнуть прямо туда, но лучше делегировать это шкалам.
И конечно. На каком-то уровне надо работать с примитивами. От этого никуда не денешься. Компьютеры - это всё ещё числодробилки.
Я так понимаю, вы в последней строке имеете ввиду dispatcher, а не sensor?
Нет. Сенсор и отдаёт Temperature, но и диспетчер может отдавать Temperature, тут суть в том, что никто не отдаёт "голый double". (Хотя на практике это сплошь и рядом происходит, ибо это удобно как для быстрого порождения объекта Temperature, так и для использования его значения в виде double).
И конечно. На каком-то уровне надо работать с примитивами. От этого никуда не денешься
Суть инкапсуляции то, поди - загнать этот уровень глубоко вниз и мало кому показывать что там происходит.
Суть инкапсуляции то, поди - загнать этот уровень глубоко вниз и мало кому показывать что там происходит.
Неожиданно. А вот у меня в эрланге, где пользовательских типов не существует, есть только восемь стандартных, — мне, получается, инкапсуляция недоступна?
Почему? Там же там есть модули и структуры. В elixir точно были. Те же модули могут иметь приватные методы.
Суть инкапсуляции, в моем понимании, это отделить поведение (контракт) от его реализации. В разных языках это может делаться по разному. Так-то как в python можно договориться и решить, что методы c _ являются приватными и не вызывать их извне. Но лучше, конечно, когда язык имеет необходимые конструкции и механизмы.
Модуль — это набор функций, у него нет данных. Структура — это map
с синтаксическим сахаром.
Суть инкапсуляции, в моем понимании, это отделить поведение (контракт) от его реализации.
В этом суть полиморфизма, а не инкапсуляции.
Функции модуля и сама структура (которая суть просто синтаксический сахар, как я уже сказал, объявление структуры — это добавление в мапу поля {:__struct__, ModuleName}
, и ровным счетом — ничего больше) — две вообще никак не связанные между собой штуки.
Но ведь, как только в модуле появляется функция с аргументом %ModuleName{}, у вас сразу же появляется эта связь. Приватные функции модуля для работы с этой структурой вызвать нельзя. Вот вам и сокрытие поведения.
как только в модуле появляется функция с аргументом
%ModuleName{}
В любом модуле. И матчинг на %ModuleName{}
вообще ничем не отличается от матчинга на %{__struct__: ModuleName}
. Приватные функции нельзя вызывать, это правда, но структуры здесь ни при чем.
Есть гипотеза, что вы не понимаете, как работают структуры в эликсире, и зачем они в принципе нужны. К сокрытию данных/поведения они никаким боком не причастны.
Обычные map с сахаром. Я понимаю, elixir гибкий язык и можно все раскидать по функция\модулям как угодно. Но struct + module + matching дает то самое сокрытие поведения. Если вы сами это захотите сделать.
Посмотрите на исходники Elixi, Ecto. Там что нет модулей с defstruct и приватными методами? Нет сокрытия?
У меня куча коммитов и в корку языка, и в экто. Я видел их исходники, а частично — писал.
Сокрытие в эликсире и эрланге реализуется исключительно акторной моделью, врутренним стейтом процессов. Не модулями и тем более не структурами.
На каждый чих GenServer делать? Это антипаттерн. Плюс наличие тех же протоколов и defimpl буквально говорит об отделении реализации. Если это не сокрытие, то я уже и не знаю что тут ещё сказать.
На каждый чих GenServer делать? Это антипаттерн.
Вы бы почитали, для собственного развития, что там по ссылке написано. А написано вот что:
A process itself does not represent an anti-pattern, but it should only be used to model runtime properties (such as concurrency, access to shared resources, error isolation, etc). When you use a process for code organization, it can create bottlenecks in the system.
Выделение — моё. Если вам нужна в эликсире инкапсуляция — без процесса не обойтись (процесс — это не обязательно GenServer
, кстати).
наличие тех же протоколов и defimpl буквально говорит об отделении реализации
Это не сокрытие реализации (они открыта и вы можете вызвать ее напрямую, как вот тут, например), это полиморфизм.
Вы, к сожалению, не понимаете, как устроено OTP, поэтому зря пытаетесь спорить.
Для изоляции состояния потребуется. GenServer, Agent, Task не суть. Для поведения не обязательно.
Вы же вызываете имплементацию через протокол. Вам известен только он (интерфейс). Что там в реализации вы не знаете. Я вам ещё раз напишу:
Инкапсуляция служит для того, чтобы изолировать контрактные обязательства абстракции от их реализации.
Полиморфизм помогает этого достичь скрывая поведение за интерфейсом
GenServer
,Agent
,Task
У Task
нет внутреннего состояния, Agent
— мертворожденная сущность (поверх GenServer
, кстати), созданная ради разделения выполнения кода по вызывающему и вызываемому процессам (Валиму 12 лет назад показалось, что это крутая идея, а отговорить было тогда некому).
Я имел в виду вот такой процесс:
echo = spawn_link(fn ->
receive do
{:ping, pid} -> send(pid, :pong)
end
end)
send(echo, {:ping, self()})
receive do
:pong -> IO.puts("Echoed")
end
# Echoed
Ага. Написал по памяти. Что вспомнилось.
Я все это к тому, что в элексире хватает различных механизмов, чтобы поинкапсулировать в свое удовольствие.
А вот у меня в эрланге, где пользовательских типов не существует, есть только восемь стандартных, — мне, получается, инкапсуляция недоступна?
Ок. Загоним с помощью ChatGPT инкапсуляцию глубоко:
public class Sensor {
// Публичный статический метод, создающий объект Temperature
public static Temperature getTemperature() {
double value = 23.0; // например, измерено датчиком
return new Temperature(value);
}
// Вложенный класс Temperature
public static class Temperature {
private double celsius;
// Приватный конструктор
private Temperature(double celsius) {
this.celsius = celsius;
}
public double toCelsius() {
return celsius;
}
public double toFahrenheit() {
return (celsius * 9 / 5) + 32;
}
public double toKelvin() {
return celsius + 273.15;
}
}
}
Теперь на Erlang:
-module(sensor).
-export([get_temperature/0, to_celsius/1, to_fahrenheit/1, to_kelvin/1]).
%% Создание объекта температуры (только через sensor)
get_temperature() ->
Value = 23.0, % эмуляция измерения
create_temperature(Value).
%% Приватный "конструктор" (не экспортируется)
create_temperature(Celsius) when Celsius >= -273.15 ->
{temperature, Celsius};
create_temperature(Celsius) ->
erlang:error({invalid_temperature, Celsius}).
%% Методы Temperature
to_celsius({temperature, C}) ->
C;
to_fahrenheit({temperature, C}) ->
(C * 9 / 5) + 32;
to_kelvin({temperature, C}) ->
C + 273.15.
И использование:
1> c(sensor).
{ok,sensor}
2> T = sensor:get_temperature().
{temperature,23.0}
3> sensor:to_celsius(T).
23.0
4> sensor:to_fahrenheit(T).
73.4
5> sensor:to_kelvin(T).
296.15
6> Bad = sensor:create_temperature(-300).
** exception error: undefined function sensor:create_temperature/1
7> sensor:get_temperature(-300).
** exception error: {invalid_temperature,-300}
И итог от ChatGPT:
Что мы имеем:
Чёткая инкапсуляция: Temperature создаётся только через get_temperature/0
Temperature — это tuple {temperature, Celsius}
Ошибка при попытке создать недопустимую температуру
Если нужно — можно расширить модуль и добавить другие "сенсоры", температуру в Реомюрах или методы сравнения температур.
Никогда не пытайтесь использовать LLM там, где ваших знаний недостаточно (в остальных случаях от нее и толку нет).
Смотрите, как я легко получу некорректную температуру для передачи её куда угодно безо всяких конструкторов: {temperature, -300}
. Voilà.
Инкапсуляция в акторной модели эрланга достигается при помощи процессов. «Приватные» aka неэкспортируемые методы тут ни при чем. Вы скрыли от внешних вызовов создание кортежа, который даже ребенок способен создать руками.
Смотрите, как я легко получу некорректную температуру для передачи её куда угодно безо всяких конструкторов: {temperature, -300}. Voilà.
Ого, как легко в Erlang "взломать" обьект то. - ок, процесс так процесс:
-module(sensor).
-export([start/1, to_celsius/1, to_fahrenheit/1, to_kelvin/1]).
%% Запуск нового "объекта" Temperature (процесса)
start(Celsius) when Celsius >= -273.15 ->
spawn(fun() -> loop(Celsius) end);
start(Celsius) ->
erlang:error({invalid_temperature, Celsius}).
%% Интерфейс
to_celsius(Pid) ->
Pid ! {to_celsius, self()},
receive
{celsius, C} -> C
end.
to_fahrenheit(Pid) ->
Pid ! {to_fahrenheit, self()},
receive
{fahrenheit, F} -> F
end.
to_kelvin(Pid) ->
Pid ! {to_kelvin, self()},
receive
{kelvin, K} -> K
end.
%% Внутренний процесс (инкапсулирует температуру)
loop(Celsius) ->
receive
{to_celsius, From} ->
From ! {celsius, Celsius},
loop(Celsius);
{to_fahrenheit, From} ->
F = (Celsius * 9 / 5) + 32,
From ! {fahrenheit, F},
loop(Celsius);
{to_kelvin, From} ->
K = Celsius + 273.15,
From ! {kelvin, K},
loop(Celsius)
end.
Применяем:
1> c(sensor).
{ok,sensor}
2> T = sensor:start(23.0).
<0.94.0>
3> sensor:to_celsius(T).
23.0
4> sensor:to_fahrenheit(T).
73.4
5> sensor:to_kelvin(T).
296.15
6> sensor:start(-500).
** exception error: {invalid_temperature,-500}
Заключение от ChatGPT:
Temperature теперь — процесс с приватным состоянием (loop/1)
Внешний код не имеет доступа к значению напрямую
Всё общение только через сообщения
Температура проверяется на этапе создания
Это уже практически объект в стиле ООП, но на Erlang'овский лад — через процессы.
Это уже практически объект в стиле ООП, но на Erlang'овский лад — через процессы.
Ох, так громко и неприлично отфыркиваясь — я не ржал давно.
А еще я просто не могу всерьёз относиться к людям, которые разговаривают с LLM. Черта характера, извините уж.
я просто не могу всерьёз относиться к людям, которые разговаривают с LLM. Черта характера, извините уж.
Сегодня инфа пришла:
Учёные доказали, что ChatGPT может ставить психиатрические диагнозы
ИИ показал высокую точность в диагностике психиатрических заболеваний, не допустив ошибок при анализе клинических описаний. Он также успешно предложил стратегии лечения.
Исследователи считают, что ChatGPT может стать полезным инструментом для психиатров и повлиять на развитие медицины. Ранее ИИ уже прошёл лицензионный экзамен для получения медицинской лицензии.
Соглашусь с автором. Инкапсуляция она про отделения контракта от реализации.
В С++ частью контракта могут быть и исключения и коды ошибок и какие-то ограничения на передаваемые значения и даже ассерты внутри реализации. К сожалению, кроме как комментариями к описанию класса эти контракты описать бывает невозможно.
Ребята, одержимые функциональщиной опять пытаются доказать, что лесенки и индуский код это лучше чем код разложенный по полочкам и переиспользуемый, за счет ооп?
С чего вы взяли, что я как-то специально одержим функциональщиной? Парадигм в этом мире больше двух, ФП меня как основной инструмент не устраивает, хотя я и умею писать без лесенок и индусского кода (например, через меня не пройдет PR, в котором в принципе есть условные операторы if
).
Я прошу прощения конечно, лично на вас я не гоню, просто на хабре стал замечать много статей, где пытаются поносить ООП в пользу фукционала. И данная статья мне опять показалась такой же.
Лично мне кажется, что новочки(это не про вас) ошибочно думают, что функционал проще, так как там не надо изучать всякие SOLID и прочее.
Плюс на собесах часто стали про эту хрень спрашивать и это конечно всех бесит.
Поэтому и проще топить за функционал.
Но лично я считаю это все это ошибки новичков. ООП не так сложно понять, надо просто покатать на нем пару лет плотно, на разных задачах, и еще лучше если в самом начале начать изучение именно с функуиональных языков, а потом плавно катать на ООП языках, что бы понять плюсы ООП.
PS.
SOLID, по моему, вообще мало кто практикует на полную, но как буд-то по ошибке выжевшего, в блогах видно только тех людей, кто пишет про с SOLID.
Я покатал на ООП лет 15 более, чем плотно (а если считать c++ и дельфи — то и все 25), и смею заверить: я лично вижу в ООП больше минусов, чем плюсов.
Это не значит, что плюсов не существует. Это значит, что я считаю устоявшийся набор паттернов — избыточным и местами противоречивым, а SOLID — попыткой покрыть сводом правил некоторую ограниченную кругозором авторов эмпирику. У каждой парадигмы есть границы применения (иначе их давно свернули бы в одну, идеальную). У ООП — они тоже есть, и я говорю именно о них.
Потому что обратной стороной того, что видите вы («стал замечать много статей, где пытаются поносить ООП в пользу фукционала») — является то, что вижу я: применение ООП там, где его минусы выпирают с первой же строки кода, но это всё равно ООП, просто потому, что кроме ООП люди в принципе ничего в своей жизни не видели.
И в современном мире антагонистом ООП выступает не ФП, конечно же, а акторная модель.
"Но даже в тривиальном примере выше есть проблема: нам надо бы обработать случай value < -273.15°C. И вот тут-то инкапсуляция и отыквится. Приведенный пример вроде бы можно починить: либо выбросив Exception из setCelcius, либо вернув boolean вместо void."
Либо сделать обработку данных в конструкторе, ваш класс больше ничего не делает кроме записи и отдачи температуры.
Лайфхак: пишите на Хабре что ООП это плохо и фронтендеры будут вам ставить лайки
Можно ли реализовать инкапсуляцию средствами ООП?