Опишите несколько популярных паттернов проектирования (Singleton, Strategy, Observer): когда их применение оправдано, какие анти-паттерны они могут породить при неправильном использовании и как их адаптировать в функциональном стиле
Singleton - Назначение / когда оправдан: гарантировать существование ровно одного экземпляра некоторого ресурса с глобальной точкой доступа (напр., конфигурация приложения, пул соединений), когда создание дополнительных экземпляров недопустимо или дорого и контролируется на уровне процесса/модуля. - Анти‑паттерны при неправильном использовании: превращается в глобальное состояние / скрытую зависимость, затрудняет тестирование (нужна жесткая подмена), вызывает проблемы с инициализацией и конкурентностью, усложняет расширение; часто служит "скрытым" God‑object. - Адаптация в функциональном стиле: вместо глобального мутируемого объекта — использовать неизменяемую конфигурацию, передаваемую явно в функции (dependency injection через параметры), или ленивую инициализацию в модуле, возвращающую чистую фабрику/контекст. Если нужен управляемый эффект (единственный ресурс), представить его как значение в замыкании или как возвращаемую «капсулу» с явными операциями; для тестирования — подменять контекст простыми значениями/фиктивными реализациями. Strategy - Назначение / когда оправдан: вынести изменяемую часть поведения в отдельную семейство реализаций, чтобы менять поведение без изменения клиента (плюс облегчение расширения). - Анти‑паттерны при неправильном использовании: чрезмерная фрагментация логики на десятки маленьких стратегий, дублирование кода между стратегиями, использование стратегий как тонких фасадов над switch/case (не решая проблему), хранение состояния внутри стратегий, что возвращает побочные эффекты и усложняет композицию. - Адаптация в функциональном стиле: стратегии — это просто функции первого класса (поведение передаётся как аргумент). Преимущества: легко комбинировать через композицию, тестировать, чистые функции не имеют скрытых состояний. Для сложных конфигураций использовать частичную функцию/каррирование или возвращать новую функцию с замкнутым неизменяемым конфигом. При необходимости состояния — хранить его вне стратегии и передавать явно (или использовать чистые состояния + возвращать новый state). Observer - Назначение / когда оправдан: уведомлять множество заинтересованных компонентов об изменениях состояния или событиях без жесткой связи издателя и подписчиков (UI‑уведомления, события домена). - Анти‑паттерны при неправильном использовании: утечки памяти из‑за неотписавшихся подписчиков, неопределённый порядок вызовов и скрытые зависимости, «штурм» событий (event storm) приводящий к непредсказуемым цепочкам побочных эффектов, сложности с обработкой ошибок подписчиков, гонки в многопоточном окружении. - Адаптация в функциональном стиле: использовать потоковую модель (FRP, Rx‑style streams) или иммутабельные очереди событий; подписка дает возвращаемую функцию/значение для отписки (явное управление ресурсами). Обработчики — чистые функции от события к новому состоянию (или к описанию эффекта), а последовательность и согласованность достигаются через композицию потоков (map/filter/scan) и явную последовательную отработку. В императивных языках можно моделировать издателя как чистую функцию, принимающую список обработчиков и возвращающую новый список обработчиков при подписке/отписке. Короткие практические рекомендации (универсально) - Предпочитайте явные зависимости (параметры, контексты) скрытым глобальным состояниям. - Отдавайте предпочтение чистым функциям и композиции; состояние — извне и иммутабельно или явно возвращаемое. - Для ресурсов с единственным экземпляром моделируйте «единственность» как владение ресурсом (контекст/капсула), а не как глобальную переменную. - Для событий используйте структуры потоков и явные механизмы отписки, для стратегий — функции первого класса.
- Назначение / когда оправдан: гарантировать существование ровно одного экземпляра некоторого ресурса с глобальной точкой доступа (напр., конфигурация приложения, пул соединений), когда создание дополнительных экземпляров недопустимо или дорого и контролируется на уровне процесса/модуля.
- Анти‑паттерны при неправильном использовании: превращается в глобальное состояние / скрытую зависимость, затрудняет тестирование (нужна жесткая подмена), вызывает проблемы с инициализацией и конкурентностью, усложняет расширение; часто служит "скрытым" God‑object.
- Адаптация в функциональном стиле: вместо глобального мутируемого объекта — использовать неизменяемую конфигурацию, передаваемую явно в функции (dependency injection через параметры), или ленивую инициализацию в модуле, возвращающую чистую фабрику/контекст. Если нужен управляемый эффект (единственный ресурс), представить его как значение в замыкании или как возвращаемую «капсулу» с явными операциями; для тестирования — подменять контекст простыми значениями/фиктивными реализациями.
Strategy
- Назначение / когда оправдан: вынести изменяемую часть поведения в отдельную семейство реализаций, чтобы менять поведение без изменения клиента (плюс облегчение расширения).
- Анти‑паттерны при неправильном использовании: чрезмерная фрагментация логики на десятки маленьких стратегий, дублирование кода между стратегиями, использование стратегий как тонких фасадов над switch/case (не решая проблему), хранение состояния внутри стратегий, что возвращает побочные эффекты и усложняет композицию.
- Адаптация в функциональном стиле: стратегии — это просто функции первого класса (поведение передаётся как аргумент). Преимущества: легко комбинировать через композицию, тестировать, чистые функции не имеют скрытых состояний. Для сложных конфигураций использовать частичную функцию/каррирование или возвращать новую функцию с замкнутым неизменяемым конфигом. При необходимости состояния — хранить его вне стратегии и передавать явно (или использовать чистые состояния + возвращать новый state).
Observer
- Назначение / когда оправдан: уведомлять множество заинтересованных компонентов об изменениях состояния или событиях без жесткой связи издателя и подписчиков (UI‑уведомления, события домена).
- Анти‑паттерны при неправильном использовании: утечки памяти из‑за неотписавшихся подписчиков, неопределённый порядок вызовов и скрытые зависимости, «штурм» событий (event storm) приводящий к непредсказуемым цепочкам побочных эффектов, сложности с обработкой ошибок подписчиков, гонки в многопоточном окружении.
- Адаптация в функциональном стиле: использовать потоковую модель (FRP, Rx‑style streams) или иммутабельные очереди событий; подписка дает возвращаемую функцию/значение для отписки (явное управление ресурсами). Обработчики — чистые функции от события к новому состоянию (или к описанию эффекта), а последовательность и согласованность достигаются через композицию потоков (map/filter/scan) и явную последовательную отработку. В императивных языках можно моделировать издателя как чистую функцию, принимающую список обработчиков и возвращающую новый список обработчиков при подписке/отписке.
Короткие практические рекомендации (универсально)
- Предпочитайте явные зависимости (параметры, контексты) скрытым глобальным состояниям.
- Отдавайте предпочтение чистым функциям и композиции; состояние — извне и иммутабельно или явно возвращаемое.
- Для ресурсов с единственным экземпляром моделируйте «единственность» как владение ресурсом (контекст/капсула), а не как глобальную переменную.
- Для событий используйте структуры потоков и явные механизмы отписки, для стратегий — функции первого класса.