Comments 98
Начиная с какой-то версии в модуле можно определить __getattr__ и таким образом реализовывать ленивое создание объекта https://peps.python.org/pep-0562
Я так его и делаю, когда тесты не пишу. А когда пишу избегаю этот паттерн.
Мысль хорошая, но очень длинно.
TLDR: просто используйте глобальную переменную в отдельном модуле.
Ну, это, по сути, обработанные материалы лекции, где я использовал каждый кривой вариант для объяснения кучи общепитоновских вещей.
При этом ведь важно всегда это модуль импортировать одинаково?
Если где-то импортировать по абсолютной ссылке, а где-то по относительной, да еще из-за структуры директорий где-то '.', где-то '..', может беда случиться.
От такого есть защита, или только на внимательность уповать?
К счастью, такого нет, защита не нужна, она встроенная. Обычные импорты, сколько и как ни делай, дают ровно один элемент в sys.modules. Способ задания пути влияет на поиск модуля и адресацию его в конкретном месте — и только. Вот если грузить исходники руками, задавая имя модуля — да, поломать можно, но это надо именно ломать, не ограничиваясь стандартным механизмом.
GoF была хорошей книжкой 20 лет тому назад. Но сейчас от неё толку не больше, чем от египетских иероглифов. Все полезные паттерны уже реализованы в самих языках, а вредные мирно покоятся в своих гробницах. Не надо их оттуда откапывать, там нет золота - только прах, кости и древние проклятия с граблями.
Это, на мой взгляд, не так. Эти паттерны вообще не про языки, хотя реализуются в разных языках иногда по-разному. Скажем, какой-нибудь Visitor как использовался для обхода и преобразования AST в парсерах и компиляторов 30 лет назад, так и используется. Итераторы и фабрики как были, так и есть (и наличие в Python стандартного протокола итератора ничего не меняет, полно и нестандартных реализаций). Прототипы, адаптеры — что поменялось? И всё так же. ООП изменилось не так сильно, менялись в основном языки. Никуда эти идеи не делись, просто опыта их реализации добавилось, появились новее решения. Это не так ново, как в 90-х, когда я это прочитал, но по-прежнему существует.
Книжка была интересной как рефлексия опыта разработки тестового редактора на технологиях того времени. Авторы показали подход как находить некоторый класс эмпирических закономерностей в своем коде, документировать и возможно использовать в будущем. Но они совершенно не настаивали на том, чтобы их паттерны стали догмами и потом разработчики искали в каждой программе возможности для их тиражирования.
Рефлексия это интеллектуальный труд. Взять примеры из книжки - большого ума не надо. Новички часто хватаются за второе, как более простое, и начинает создавать синглтоны, потокобезопасные синглтоны, асинхронно инициализируемые синглтоны, универсальные синглтоны. Короче заниматься решением проблем, которые вообще не нужно создавать.
Уберите из лексикона все то, что не относится к решаемой задаче и вы получите именно то решение, которое будет легко понимать и сопровождать.
Реализации меняются. И сильно. Примеры тут — это вообще не напоминает примеры в GoF, но ключевое в их паттернах — идеи. Итератор — не языковая конструкция, это идея отделить контекст итерации от итерируемого контейнера. Синглетон — это не про функцию, возвращающую свою статическую переменную, как это часто делали в C++. Это про саму идею объекта, существующего в одном экземпляре.
О самих паттернах я тут не пишу. Я пишу, как это можно (и как не надо) их реализовывать в современном Python, это более прикладная вещь.
И да, создавать эти проблемы чаще всего не нужно. Но иногда они возникают, что бывает довольно неприятно, особенно если в голове нет наработанных решений для всего этого. Паттерны, реализация паттернов — для этого. Прикладная разработка — про решение ровно своих задач, используя ту поваренную книгу, которая наработана в голове. И там далеко не только GoF, одного GoF очень мало. Там все наши кодировочные идиомы. Ты масса архитектурных решений, которые мы видели в своей биографии, осмыслили — и стали применять на свой манер. Разноуровневых паттернов много. Иногда мы расширяем эту поваренную книгу, когда решаем что-то новое.
Вот буквально на днях я писал парсер языка на PLY. И понял, что мне нельзя запихать в yacc полную грамматику: сообщения об ошибках становятся невразумительными. И я стал парсить код как набор операторов, который иерархически организован в блоки. Каждый оператор парсится, структура блоков тоже. Собираю AST, даже если сочетания операторов бредовые и бессмысленные. А сгенерировал AST — пошёл по нему визиторами и там ВРУЧНУЮ проверил осмысленность сочетаний, выдавая осмысленные ошибки, а не «Syntax error near token '\n'» на десять строк выше места проблемы, поскольку правило не матчится именно оттуда. Если в терминах C, то сгенерированный парсер пропустит такой бред: i = i+1; {main() {return 0;}}. Паттерн можно назвать «Ограничение сферы ответственности сгенерированных парсеров». Позволяет анализировать структуру даже не вполне корректного кода. Он теперь есть в голове, в следующий раз уже знаю, что делать. не вполне корректного кода, даёт осмысленные сообщения об ошибках. Есть сфера применения, есть решение. Язык реализации и средство автогенерации парсеров по грамматике значения не имеют. Вполне личный паттерн. Правда, уже узнал, что так иногда и другие делают. Значит, вообще нормальный паттерн. Для тех, кто пишет парсеры. Будет в моей кулинарной книге и дальше.
И, кстати, эта конкретная статья ровно про предостережение, чего делать НЕ НАДО. Я намеренно собрал рекомендации, которые считаю — в основном — идиотскими (Но они очень стойкие. Каждый второй лепит подобное на каком-то этапе. Спросишь ChatGPT — обязательно посоветует их.) С объяснением, что все эти реализованные руками потокобезопасности и прочее не так универсальны, и если уж язык предоставляет готовое подходящее средство, использовать надо именно его, а не DCL и прочую хрень.
Во время когда GoF написали свою книжку возможности C++ и Java были куда скромнее. Паттерны фактически показывали способы как обходить типичные ограничения языка в парадигме ООП. С точки зрения современных языков они выглядят довольно низкоуровневыми. Ну и избыточными - зачем городить такой огород, когда всё уже есть из коробки? Наличие функций высшего порядка, генераторов и библиотеки абстрактных типов, как в python, даёт возможность просто писать код, не замечая, что у других тут могут быть какие-то особенные сложности.
По-моему ценность GoF больше в том, что они вдохновили других авторов по аналогии разбирать и искать типичные решения типичных проблем в своих областях. Книги, которые рассматривают не ООП, а различные архитектуры: Enterprise Integration Patterns, Reactive Design Patterns или Microservices Patterns были уже полезными.
Книг было довольно много. Была ещё в девяностых серия книг про архитектурные паттерны, паттерны параллельного и распределённого программирования и прочее. Это паттерны на других уровнях абсиракции, и архитектору без этого, спору нет, никак.
Просто, условно, алгебра не отменяет арифметики. Паттерны GoF — это идеи уровня кода и организации групп масштаба нескольких классов. Под ними — кодировочные, над ними — уровня архитектуры. А есть ещё паттерны, применяемые в специфических областях, а не общего назначения.
И всё равно мышление разработчика развивается от простого. Сейчас большинство этих паттернов у почти любого квалифицированного разработчика давно на уровне неосознанной компетентности. Но оно не сразу туда попадает. Нет смысла считать ерундой то, что является для тебя пройденным этапом. Я тоже плотно изучал это в 90-х. Это было полезно, хотя, естественно, это не было последней прочитанной мною книжкой и венцом профессионального развития. :-)
Полностью не согласен. GoF паттерны - это не магия, это язык для коммуникации между разработчиками. Да, многие реализованы встроенными средствами, но нужно понимать, ЧТО ты используешь и ПОЧЕМУ. Singleton, Observer, Factory - это не просто наборы кода, это определения проблем и решений. Разработчик, который не знает этих паттернов, просто переизобретает их заново каждый день, но хуже.
Со статьёй или с предыдущим оратором? Я-то тоже поныне всё это использую.
Понятийный аппарат сильно обновился с тех пор:
Билдер и адаптер используются. Итератор только как часть языка. Фабрика как конструктор.
Сейчас говорят
Observer - pub/sub, events, reactive streams
Chain of Responsibility - middleware, pipeline
Decorator - middleware, wrapper
Command - handler, action, callback
Strategy - policy, просто "передай функцию"
State - state machine, FSM
Visitor - (просто не говорят, делают match)
Facade - API, client, wrapper
Proxy - middleware, interceptor
Новый понятийный аппарат, которого у GoF нет тоже разросся. Например:
Streams - потоки данных (reactive)
Actors - изолированные сущности с mailbox
Channels - коммуникация между задачами
Effects - контролируемые сайд-эффекты (ФП)
Combinators - композиция функций
От того, что вы что-то переименовали, а для чего-то предложили другие способы реализации, идеи поменялись. Кстати, набор паттернов расширяется, далее паттерны того же уровня, что GoF, сейчас есть и новые.
Итераторы, например, не часть языка. В Python есть, скажем, протокол итератора. В 90% случаев, конечно, его и надо реализовывать. Но не всегда. Полно объектов с методом next(), например. И иногда это даже оправдано.
Собственно, до итераторов, надеюсь, дойду, пока нет смысла обсуждать.
А вот Observer? До сих пор на многих языках полно интерфейсов с методом on_что-то-там(), и это старый добрый Observer. Хотя есть другие способы нотификации объектов о событиях, но никакой не универсален. А сама идея жива.
Синглтон противоречит ООП. Это не объект. Объект имеет жизненный цикл, синглтон не имеет.
Если хотите в питон что-то на всю жизнь программы, то создайте иммутабельно где-то в main прокидывайте в DI
GoF устарели ужасно. Согласен, решение обычно уже встроено в язык.
Большинство GoF — это обходы ограничений C++/Java того времени:
- Итератор встроен везде
- Стратегия это функции высшего порядка
- Команда это замыкание
- Фабрика часто просто функция
Актуальными остались немногие: Composite, Decorator (частично), State (иногда). И то — сильно проще реализуются.
Влашин об этом хорошо: в ФП половина паттернов исчезает, потому что функция — уже и стратегия, и фабрика, и команд
Я, собственно, написал, для чего синглетон можно и нужно использовать (например, корневой интерфейс библиотеки, желательно — без состояния) и почему обычно его стоит избегать. И это очень узкая область. Никакому ООП он не противоречит совершенно, просто у него сейчас очень мало валидных сценариев использования. И потом, как можно не иметь жизненного цикла? И как синглетон может не быть объектом? У него просто специфический жизненный цикл.
А что паттерны GoF устарели — не согласен категорически. ООП (не языки) изменилось за 30 лет мало. Подозреваю, вы просто не нуждаетесь во многих паттернах в повседневной деятельности.
Мой личный топ — Builder (а как собрать, скажем, картографический объект, где куча возможных элементов?), Visitor (а как ещё после парсинга грамматики работать с AST?), Observer (это вообще в современном ПО часто), Iterator (на каждом шагу), Chain of Responsibility (в обработке запросов очень частый), Adapter. Это только то, что постоянно используется. Есть редкие, парочку вообще никогда не пришлось использовать в реальном ПО — Bridge, скажем.
И никуда это не уходило. Просто очень много теперь есть и других паттернов. И архитектурных (уровнем повыше), и кодировочных (уровнем пониже). Паттерны — это вообще стандартные решения. И паттерны GoF вполне живы, просто много есть других паттернов.
паттерны GoF вполне живы
Как задачи живы, конечно. Но современное решение ну совсем не как в книжках GoF. А иногда насколько тривиальное, что и говорить не о чем.
Builder это вообще ФП по определению. ООП тут костыль.
Enum + match вместо Visitor/State/Strategy
Closures/функции как жители первого класса вместо Command/Strategy
Iterator встроен в язык Rust
Chain of Responsibility через оператор "?"
Вот честно, пришлось даже прикладывать немалые усилия чтобы вспомнить что стоит за каждым названием, а вот реализация на Rust вспомнилось сразу. Да и на c# не очень сложно, но не как в книжках
Собственно, реализация и должна меняться. Она изначально была разная в разных языках. Я потому этот цикл задуман.
Что до встроенных итераторов — опять же, это языковой механизм. В Python вот тоже есть протокол итератора, но это не значит, что нельзя сделать класс с методом next(). Изредка это даже имеет смысл.
А вот почему builder — это ФП? Вы говорите о реализации его цепочкой вызовов? Так цепочка вызовов — это не суть паттерна. Суть в том, что есть "рабочий стол" (объект-builder), есть способы "прикрутить" к собираемому новые части, а в конце — забрать результат. Когда-то я много писал конвертацию карт. И там было полно билдеров вообще без цепочек вызовов (ты изначально не знаешь, сколько и чего добавишь, цепочку не написать). Имхо, если правильно понял вас, именно цепочки напоминают ФП. Но это просто один способ оформления паттерна.
Но в принципе мы об одном — способы реализации в языке очень эволюционируют и подстраиваются под реальность. Поэтому чуть не под каждый язык можно писать книжку, как там реализовать паттерны.
Билдер основан хорош на иммутабельности, поэтому концептуально он ФП.
Хорошо, не обязательно. Но я уже привык
// Mutable builder тоже валидно
let mut b = Builder::new();
b.name("x");
b.port(8080);
let result = b.build();
// Immutable/consuming — ближе к ФП
let result = Builder::new()
.name("x") // self - Self
.port(8080) // self - Self
.build(); // self - TОба работают. Но второй лучше:
Нет промежуточного мутабельного состояния
Цепочка трансформаций
Нельзя случайно переиспользовать builder после build()
Погодите, каким образом именно паттерн, а не один из способов реализации? Это лишь один из нескольких вариантов. Вот, кстати, хорошо, что заговорили, надо будет об этом в следующей статье написать.
Самая частая моя реализация — есть, скажем, CrtObjectBuilder. Его инициализируешь, потом постепенно наполняешь, вызывая всякие add_attr(), add_id(), add_ref(), add_points() неизвестное количество раз, он копит. Потом зовешь build(), тот возвращает CrtObject, пакуя данные. В этом варианте вообще нет иммутабельности, но это тоже билдер.
Это хорошо. Но когда вы знаете, какой набор компонентов планируете добавить. А в случае с построением картографического объекта я часто этого вообще не знал. Допустим, получил от читалки исходного формата — добавил. Там это всё накапливается. Это тот случай, когда иммутабельность будет как минимум не очень эффективна. И реально долго заполняются контейнеры компонентов. Иногда добавление вообще в Observer.
Так что вариант в стиле ФП хорош, но он не покрывает 100%. В вашем примере он, наверное, идеален, но в моём — непригоден.
Как enum + match заменяет Стейт, когда он буквально создан для того, что бы уйти от всяких свичкейсов и их ограничений.
PS Стейт наверное один из самых красивых паттернов (и самый усложняющей код)
Усложняющий если писать по GoF
struct State {
virtual void handle(Context& ctx) = 0;
};
struct Done : State {
void handle(Context& ctx) override { /* stay */ }
};
struct Running : State {
void handle(Context& ctx) override {
ctx.setState(new Done());
}
};
struct Idle : State {
void handle(Context& ctx) override {
ctx.setState(new Running());
}
};Вместо этого даже c++ может красиво написать
enum class State { Idle, Running, Done };
State handle(State s) {
switch (s) {
case State::Idle: return State::Running;
case State::Running: return State::Done;
case State::Done: return State::Done;
}
}Стейт, устанавливаемый снаружи! Что дальше? Стейт на операторах goto?
Ну, вообще они от свичей уходили. Для сложной стейк-машины это очень много ошибок даёт. На трёх состояниях, конечно, и так можно. Если честно, в Питоне нет нужды создавать объекты, если от стейта нужен только handle, а не целое семейство методов. Функции на состояние совершенно достаточно. А в сочетании с генератором или async можно реализовать весьма сложную логику внутренних стейтов и иначе — это если у вас, как часто рисуют на диаграммах UML, внутри состояния есть своя машина.
Кстати, стейт в лексерах lex/yacc делается в одном объекте, но разными наборами методов.
Фишка стейта не в самих хендлерах, которые действительно можно свичкейзом, а в SwitchState, AtEnter, AtExit методах
Enter/Exit — это размазывание логики перехода между двумя местами. С SwitchState - тремя. Переход знает откуда и куда, пусть там и живёт вся логика.
Исключение — если реально нужны инварианты: "при любом входе в Running запусти таймер". Но чаще это признак что состояние слишком крупное или переходов слишком много.
Мне кажется, вы перепутали два уровня. В языке появились механизмы для более простой реализации паттернов. Это не значит, что паттерны потеряли смысл. Паттерн — это мотивация плюс решение. От того, что для реализация команды вы стали использовать замыкание, паттерн никуда не делся. Замыкание — это не сам паттерн, это лишь языковой механизм для его воплощения. У него ещё много применений. И книга GoF больше про саму идею паттернов, этот слой там не устаревает практически.
- Итератор встроен везде
и тем не менее надо понимтаь что это, когда полезете реализовывать кастомный
- Стратегия это функции высшего порядка
Можно всё свести к передаче функций в функции, польза паттернов GoF в том что они приводят примеры как в разных ситуациях похожие штуки меняются.
- Фабрика часто просто функция
у GoF нет просто фабрики, там абстрактная фабрика. Польза в абстракции.
в ФП половина паттернов исчезает, потому что функция — уже и стратегия, и фабрика, и команд
Так можно и про ООП сказать, всё объект, паттерны лишь описывают наследолвание да композицию. Можно абстрагировать до потери смысла, только пользы от этого ноль. Польза паттернов же есть - они приземляют очень общие механики до конкретных способов применения. Понятно, что в ФП надо приземлять чуточку по другому, но сценарии использования всё равно бывают разные.
Я соглашусь, что в некоторых местах имеет смысл перекомпоновать паттерны (например мне всё ещё непонятно почему lazy proxy и прокси с проверкой доступа - один паттерн, а декоратор - другой), но карданально это не изменит. А вот синглтон - практически единсвтенная вещь там которая вреда приносит больше чем пользы (даже если предположить что польза есть).
Против DI ничего не имею - отличная вещь, всем рекомендую вместо синглтонов
GoF устарели ужасно. Согласен, решение обычно уже встроено в язык.
Я смотрю на это с другого ракурса, некоторые паттерны настолько хороши и актуальны, что их включили в спецификацию языка.
Аналогично.
Поэтому я не воспринимаю их как паттерны а как часть языка
Я тоже смотрю чуть с другого. В спецификацию языка включили не паттерны, а средства их реализации. Скажем, асинхронное выполнение кода — тоже вполне себе паттерн. Очень долго всё было на колбэках. А потом в языки один за другим стали пролезать способы писать этот код совсем иначе, даже до C++ уже доехало. Были паттерны (повыше уровнем, чем GoF, всякие реакторы и проакторы) — появилось средство их удобной реализации.
from typing import Final
class Settings:
...
settings: Final[Settings] = Settings()В питоне вы можете свободно импортировать экземпляр класса, Final не позволит переприсвоить значение в settings.
Это никак не помешает вам создать ещё один экземпляр класса, но немного осмысленности и ваш код в разы более читаемый и интуитивный
Синглтон. Помню, на прошлой работе коллега сначала вкорячивал синглтон, а потом его убирал, т.к. оно тестам мешает. Лично я пришёл к выводу, что лучшая реализация - это конвенция. Инициализируешь в модуле и говоришь всем, что settings менять нельзя. А в тестах можно делать, что хочешь. С ..._imp.py хорошая идея, в тех же тестах можно использовать.
Ну и касаемо недолюбливания глобальных объектов и синглтона. В том же фласке вполне себе есть глобальные объекты-синглтоны и там с этим очень удобно работать.
Да, случаев, когда он нужен, немного. Settings — действительн сами по себе синглетон. И да, инициализация в модуле и соглашения. В случае с settings я часто использую соглашение, что все донастройки settings — в главном модуле, в функции main и только до начала инициализации содержательной части системы (ну или в главной запускалке тестов — тоже до какой-то содержательной активности). Больше никто этого делать не должен. С чем стартовали, с тем живём. Ну, скажем, после разбора параметров командной строки и просмотра окружения выставил я язык локализации — и начал поднимать компоненты. И всё, дальше туда не лезть, только читать.
Можно, конечно, запретить запись в settings централизованно. ))) Но одно дело — в сами поля settings, а вот каждый элемент каждого словаря или списка замучишься защищать. Так что, если в проекте нет толпы джуниоров, можно не париться.
Если есть mypy или что-то подобное, то можно защитить при помощи аннотаций типов. Поставить на объект типа dict аннотацию только для чтения https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping и ловить все изменения. Это вполне бюджетно.
Кстати, отличная идея. У меня mypy во всех актуальных проектах встроен в сборочные пайплайны, так что для меня она и не стоит почти ничего.
Есть, конечно, возможность преобразовать тип, но это уже будет не глупость по недомыслию, а ярко выраженное вредительство, от такого защититься сложно. :-)
Раз пошла такая пьянка... (с)
Что с ty от astral-sh? Уже можно использовать для мелких целей? Поскольку в Antigravity все равно нет pylance, так может использовать не только uv и ruff, но и ty как в IDE, так и для проверки типов?
Я пока не пробовал. Как в своё время стал юзать mypy, так поныне и юзаю. Если услышу, что что-то сильно лучше появилось, попробую, но пока только знаю, что есть несколько других чекеров — и всё.
В шаблоне проекта у меня mypy, flake8 как линтер, наконец black для форматирования.
Лишний раз экспериментировать с инфраструктурой, если нет наводки, что что-то уж очень хорошо, мне лень.
Вот мой примерный шаблон проекта для генерации исполняемого модуля через PyInstaller, там много старья типа запускалки тестов, написанной почти 20 лет тому. Но работает, а работает — не трогай:
Я на ruff пересел, вместо flake8 и black. Устанавливаются быстрей, работают быстрей, делают тоже самое. Ну и кроме этого всю контору перевели на него и uv.
Ну, так у меня оно и так ест не так много. Black же вообще даже не в пайплайне сборки, это один запуск перед коммитом. Если б они делали что-то другое... А так-то зачем?
Если вы работаете с кодом один, тогда норм запускать только локально. Хотя мне лень запускать и я через https://pre-commit.com/ настроил.
Black — локально. А пайплайн сборки (включая PR) на GitHub Actions. Естественно, линтер и контроль типов в него включены.
То есть форматирование своих изменений каждый делает сам. Не отформатированное просто не принимается.
Не отформатированное просто не принимается.
Это делается человеком? Обычно black --check на CI запускают.
flake8
И оно, конечно, в пайплайне на PR
Вместо pre-commit можно использовать prek https://github.com/j178/prek . Он с большего совместим, быстрее, запускает проверки параллельно, проще добавлять кастомные правила.
mise, prek, poethepoet, uv, ruff, import-linter, basedpyright.
Вот мой примерный шаблон проекта для генерации исполняемого модуля через PyInstaller, там много старья типа запускалки тестов, написанной почти 20 лет тому. Но работает, а работает — не трогай:
Хотя бы ради ваших студентов обновите на то, что в тренде.
наконец black для форматирования.
Нет ничего хуже, чем black для форматирования.
У него своя философия которая экономит время при работе в команде. Чем пользуетесь вы и сколько у вас настроек не по умолчанию?
КМК, это философия совсем не подходит к Питону.
С форматтерами проблема, да. Лично мне больше всего нравился yapf, но он заброшенный и имел какие-то проблемы. В итоге остановился на autopep8. Да, не настолько функционален, зато не превращает код в нечитаемое нечто.
Мой поинт был в том, что запретить-то можно. Только гемморрно и чаще мешает, чем помогает.
И у меня settings инициализировался ровно один раз при чтении файла конфига. Всё.
Как ваш синглтон, которому вы еще и детей учите, поживает в кластере из нескольких, соединенных между собой, и общающихся (скажем, по пабсабу, чтобы вам было попроще), — нод? Ой, запускается несколько синглтонов. Ну да ничего, это же просто учебный материал.
Вы вообще о чём? Какие ноды, какой кластер? Синглетон в одном процессе, и это не имеет никакого отношения к распределённым вычислениям вообще. Как и все паттерны GoF, кстати.
P. S. Распределёнными системами я занимался, но тут-то это при чём?
При чем тут распределенные вычисления? У вас все приложения помещаются на одну машину? — Ну бывает, правда, обычно люди примерно к третьему классу средней школы из этого вырастают.
Синглетон в одном процессе […]
Тогда к чему все эти приседания с бесполезными локами и мьютексами? — Так и пишите, работает в одном процессе, на 640КБ оперативы, чего должно хватить всем.
Ну-ну. Куча кода работает вообще в одном процессе. Что-то масштабируется горизонтально, но там синглетон и нужен в каждом процессе.
И я сожалею, что, видимо, пройдя третий класс школы, вы не понимаете различия между многопоточностью, многопроцессностью и распределёнными вычислениями.
запускается несколько синглтонов
Просто одинаковая строка в конфиге каждой ноды. То есть да, но в терминах синглтонов никто не думает в таких случаях
Что нафиг значит «в таких случаях»? Вот есть у вас синглтон, который занят тем, чем обычно заняты синглтоны: что-то там апдейтит внутри себя атомарненько (read-only синглтоны адекватные люди не используют, вместо них отлично подходит статический файлик с конфигом).
Тут в ваш бизнес приходит успешный успех, люди регистрируются, как не в себя, вы решаете «горизонтально отмасштабироваться» (не по-настоящему, а как это принято в прошловековых языках, не готовых к кластеризации — просто втыкаете рядом еще один сервер, не меняя код). Всё работает, нагрузка держится, баблосы пилятся. Только то, что там внутри себя держит синглтон — на разных нодах разное, никак друг с другом не связанное, и ваше приложение вдруг начинает раздавать 100 скидок вместо запланированных пятидесяти (не надо мне про базу рассказывать, это просто пример).
Только то, что там внутри себя держит синглтон — на разных нодах разное, никак друг с другом не связанное,
начинает раздавать 100 скидок вместо запланированных пятидесяти
Что-то кажется пропущенно в середине. Я понимаю, что вы хотите проиллюстрировать, но не понимаю вашего примера.
А ещё если убрать синглтон из этих рассуждений и заменить его простым классом, то проблема не решится. Не в паттерне тут дело.
Дело всегда в паттерне, потому что само понятие «паттерн» — отдаёт нафталином и XX веком. Я вон ажно целый текст написал даже о том, почему паттерны (почти любые, кроме совсем дубовых) — несомненное зло.
Что-то кажется пропущено в середине.
Да ничего не пропущено. Синглтоны очень часто используются — как кэш базы (еще раз: не нужно цепляться к базе и предлагать ничего не кэшировать, база тут только для примера). Ну вот надо нам раздать 50 скидок. Пока мы жили на одной ноде, у нас был прекрасный, чистый и внятный код, синглтон со счетчиком, который убывает на единицу каждый раз, когда мы дали скидку. Всё работало, как часы.
Теперь мы воткнули еще три ноды — и раздали 200 скидок, 50×4, потому что у каждой ноды свой счетчик.
Если не изменяет память, паттерн у GoF это многократно проверенный на практике шаблон реализации. Паттерн состоит из названия, проблемы, контекста, ограничений, решения и примеров кода.
Т.е. прежде чем советовать другим, они использовали их в реальных проектах. Но суть в том, что даже если проблема кажется похожей, но в другом контексте или с другими ограничениями, то паттерн смысла уже не имеет (ну или как минимум его использование нужно вдумчиво обосновать, а не копировать из книжки).
GoF писали текстовый редактор для десктопа и в нем черпали вдохновение. Понятно, что ни о каких микросервисах там речи не шло. Что работало в их ситуации, в другой может и не работать. Здесь нет противоречия.
Паттерн состоит из названия, проблемы, контекста, ограничений, решения и примеров кода.
Конечно. Вот только в современном мире они состоят из названия и, если повезет, примеров кода. Проблемы, контексты, ограничения — это все за бортом.
Здесь нет противоречия.
Нет, конечно. Хотя сейчас текстовый редактор на этих паттернах тоже не напишешь (тормозить будет, как Белаз на спуске в карьер).
Микросервисы тут ни при чем. В современных реалиях даже на самом жалком ноуте — 16 ядер, а банальные монолиты — масштабируются кубером (и примерно все паттерны в лучшем случае ломаются сразу, а в худшем — превращаются в мины замедленного действия).
Кубер сразу толкает к stateless, так как при rolling update у вас будет минимум два инстанса в параллели. Под него сразу надо делать архитектуру.
при rolling update у вас будет минимум два инстанса в параллели
Инфа сотка? Эти инстансы могут уметь отличать умирающую ноду от только что родившейся. Старые и новые ноды могут жить в разных кластерах.
Под него сразу надо делать архитектуру.
Угу. И Джо Армстронг сотоварищи её уже сделали — сорок с лишним лет назад. Весь прикладной код работает из коробки, велосипеды не нужны.
Ну вот надо нам раздать 50 скидок. Пока мы жили на одной ноде, у нас был прекрасный, чистый и внятный код, синглтон со счетчиком, который убывает на единицу каждый раз, когда мы дали скидку. Всё работало, как часы.
... пока не перезапустили приложение...
Теперь мы воткнули еще три ноды — и раздали 200 скидок, 50×4, потому что у каждой ноды свой счетчик.
IMHO не в синглтоне здесь проблема.
Как бы вы обеспечивали 50 скидок из своего примера даже на одной ноде безо всяких синглтонов, в т.ч. после перезагрузки сервера?
Хочется увидеть пример, где действительно все работает как часы без переделок под увеличившееся количество нод, а если упростили что-то, притащив синглтон, поломалось.
Попросил не докапываться к необязательной базе — докопались к перезапускам. Я бы перед перезапуском (вы же обрабатываете в коде перезапуски, правда?) сливал бы остаток в локальный файл, базу, слал бы себе почтой.
Хочется увидеть пример, где действительно все работает как часы без переделок под увеличившееся количество нод, а если упростили что-то, притащив синглтон, поломалось.
Упростили что именно? Сложно отвечать на вопросы, сформулированные в стиле «как давно вы перестали пить коньяк по утрам?».
Если вам нужны примеры того, как «действительно все работает как часы без переделок под увеличившееся количество нод» — возьмите эрланг (эликсир, gleam, lfe, cure), где никто по умолчанию даже не знает, на какой именно ноде выполняется тот, или иной код.
Мой пример с кэшированием счетчика из базы при помощи синглтона — не синтетический, я лично видел такое в продакшене. Была база, в которую ходили на каждый чих. Все работало, от количества нод не зависело. Потом закэшировали, все работало, потому что одна нода. Потом добавили ноду — все поломалось. (Перед перезапуском дампились в базу, это обычная практика для горячего кэша, так-то.)
Я бы перед перезапуском (вы же обрабатываете в коде перезапуски, правда?) сливал бы остаток в локальный файл, базу, слал бы себе почтой.
Обрабатываю, но не надеюсь на то, что всегда будет корректное завершение.
И уж тем более слабо представляю, как с таким "почтовым" механизмом (да и с файлом тоже) обеспечить правильность раздачи скидок, когда станет 4 ноды.
Я не докапываюсь, я лишь пытаюсь показать, что, кмк, данный пример с кешированием значения счетчика не показывает недостатки синглтона. Там и без синглотонов проблем хватает. А с добавлением нод - вообще беда.
Если же вместо "посылаю себе почтой" я добавлю какой-нибудь Redis - это решит кучу проблем в т.ч. с увеличением количества нод. Ну так ежели я инстанс редиса буду инициализовать а-ля синглтон и использовать в разных модулях, я не вижу, почему при увеличинии количества нод внезапно все сломается.
Понятно, что в FastAPI я скорее буду активно Depends использовать, а не глобальную переменную. Но это про другое. Реконнекты, все дела...
Если вам нужны примеры того, как «действительно все работает как часы без переделок под увеличившееся количество нод»
Примеры, где все работает при увеличении количества нод, я знаю :)
Я хочу пример, как все работало как часы, прекрасно масштабировалось, а потом по каким-то внятным соображениям (а не "во-первых, это красиво") заменили на синглтон, из-за чего все сломалось, потому что именно синглтон - ужасный паттерн..
Я не понимаю, с чем вы спорите, если честно. Если мы про примитивы языка — то Redis выходит за рамки этого обсуждения. Кроме того, паттерн в вакууме не может быть «хорошим», или «плохим». Качественные характеристики ему придают варианты использования.
В эрланге я просто стартую именованный процесс с именем {:global, :singleton} — и всё работает, как надо. В питоне так сделать не получится — паттерн есть, а вариантов использования — нет. Такому паттерну — грош цена. Вот и всё.
все работало, потому что одна нода
Писали систему для strong consistency и она не заработала для eventual consistency. При том, что кажется, что это почти одно и тоже, разница огромна.
У нас на работе тоже есть такой кейс, я как раз коллегам объяснял что мы будем удивлять наших пользователей странным поведением системы.
Нет, мы можем без потери общности считать, что там осталась strong consistency. Я в курсе про сеть и прочие казусы, но в рамках данной задачи ими можно пренебречь (например, заказчик сказал, что раздать лишние купоны раз в сто лет — не страшно).
Кроме того, эрланг из коробки сделает синглтон, который будет работать в eventual consistency системах.
То что вы называете глобальный синглтон в эрланг, не имеет никакого отношения к синглтону из банды четырёх. Схожие идеи но разные условия применения.
Сервисы на любом языке можно научить общаться друг с другом и поддерживать распределённый синглтон. Только если это не идёт из коробки как в эрланге, то через редис или базу проще сделать.
если убрать синглтон из этих рассуждений и заменить его простым классом, то проблема не решится
Пардон, пропустил, когда отвечал. Простой класс не декларируют как решение проблемы, разница только в этом.
Тогда нужна одна мастер нода с изменяемым состоянием. Остальные - ее локальное кеширование.
А ваш пример, когда каждая нода выдаёт свою скидку он дурацкий и притянут за уши. То есть решить проблему можно, но проблема взялась от архитектурной ошибки.
Делается проще: скидка = сущность с ID. Генерируешь 50 записей заранее, раздача — это пометить claimed (использована). Идемпотентно, масштабируется, никакого счётчика.
В более общем случае:
Mutable shared state это трудно и главное это дорого. Чем больше нод тем дороже.
Поэтому, обычно операции изменения стейта батчат, стейт выносят куда-то наружу и он перестаёт быть ответственностью объекта.
Акторная модель удобна, но в узком горлышке становится однопоточной. Поэтому заранее, разделим на несколько стейтов по 1-5 скидок и они управляются независимо.
Do not feed the troll
Python и паттерны GoF, часть 1: Singleton