Обсудите основные паттерны проектирования (Factory, Strategy, Observer, Adapter, Singleton) — для каждой дайте пример, когда его применение уместно, и когда оно вредно
1) Factory (Factory Method / Abstract Factory) - Что это: инкапсулирует создание объектов, скрывает конкретные классы за интерфейсом фабрики. - Когда уместно: когда создание объектов зависит от конфигурации/платформы/версии и нужно выбирать реализацию в рантайме. Пример: создание DAO/репозиториев по типу СУБД (MySQL/Postgres), или фабрика виджетов для разных OS. - Когда вредно: если фабрика просто оборачивает один-конструктор без логики — добавляет лишний уровень абстракции; когда проект использует DI-контейнер, ручные фабрики часто избыточны. Альтернатива: прямое внедрение зависимостей или конфигурируемый DI. 2) Strategy - Что это: инкапсулирует алгоритм в отдельный объект, позволяет менять поведение в рантайме через интерфейс. - Когда уместно: выбор разных реализаций алгоритма по контексту (альгоритмы сортировки/валидации/кэширования, разные стратегии аутентификации). Пример: переключение стратегий кэширования (LRU, LFU, no-cache) без изменения клиента. - Когда вредно: если вариантов очень мало и нет ожидаемого расширения — создаёт много мелких классов и усложняет понимание; при частом создании объектов стратегий без кэша это накладно. Альтернатива: компактные условные ветвления или конфигурируемые функции/замыкания. 3) Observer (Publisher–Subscriber) - Что это: субъекты оповещают подписчиков об изменениях состояния, обеспечивает слабую связанность. - Когда уместно: UI-события, реактивная обработка данных, распределённые события внутри приложения, подписка на изменение модели (MVC). Пример: обновление нескольких видов представления при изменении модели. - Когда вредно: массовые подписки могут привести к утечкам памяти (подписчики не отписаны), сложно отлаживать цепочки уведомлений (неопределённый порядок), может вызвать каскадные эффекты и гонки в многопоточности. Альтернатива: явные колбэки, синхронные вызовы, stream/Reactive frameworks с управлением жизненным циклом. 4) Adapter - Что это: приводит интерфейс существующего класса к требуемому интерфейсу клиента (обёртка). - Когда уместно: интеграция с внешними библиотеками/существующим кодом, когда нельзя/не хочется менять исходный интерфейс. Пример: адаптер между старым API сервисов и новым интерфейсом приложения. - Когда вредно: когда можно просто рефакторить клиент или библиотеку — адаптер маскирует архитектурную проблему; злоупотребление адаптерами создаёт много уровней проксирования и производит накладные расходы/сложность. Альтернатива: изменение общего интерфейса, фасад при объединении нескольких интерфейсов. 5) Singleton - Что это: гарантирует единственный экземпляр класса и глобальную точку доступа. - Когда уместно: реальные глобальные ресурсы с явно одной сущностью — системный логгер, конфигурация приложения, пул подключений (но лучше управлять через контейнер). Короткий пример: readonly конфигурация, доступная везде. - Когда вредно: приводит к глобальному состоянию, скрытым зависимостям, затрудняет модульное тестирование и параллельность; проблемы с ленивой инициализацией и потокобезопасностью; для большинства случаев лучше использовать DI, явно передавать зависимости или управлять жизненным циклом экземпляра через контейнер. Краткое резюме: паттерны полезны для решения повторяющихся архитектурных задач, но вредны, когда применяются механически там, где проще и понятнее обойтись без них — выбирать их стоит исходя из потребности в расширяемости, замене реализаций и контроле жизненного цикла объектов.
- Что это: инкапсулирует создание объектов, скрывает конкретные классы за интерфейсом фабрики.
- Когда уместно: когда создание объектов зависит от конфигурации/платформы/версии и нужно выбирать реализацию в рантайме. Пример: создание DAO/репозиториев по типу СУБД (MySQL/Postgres), или фабрика виджетов для разных OS.
- Когда вредно: если фабрика просто оборачивает один-конструктор без логики — добавляет лишний уровень абстракции; когда проект использует DI-контейнер, ручные фабрики часто избыточны. Альтернатива: прямое внедрение зависимостей или конфигурируемый DI.
2) Strategy
- Что это: инкапсулирует алгоритм в отдельный объект, позволяет менять поведение в рантайме через интерфейс.
- Когда уместно: выбор разных реализаций алгоритма по контексту (альгоритмы сортировки/валидации/кэширования, разные стратегии аутентификации). Пример: переключение стратегий кэширования (LRU, LFU, no-cache) без изменения клиента.
- Когда вредно: если вариантов очень мало и нет ожидаемого расширения — создаёт много мелких классов и усложняет понимание; при частом создании объектов стратегий без кэша это накладно. Альтернатива: компактные условные ветвления или конфигурируемые функции/замыкания.
3) Observer (Publisher–Subscriber)
- Что это: субъекты оповещают подписчиков об изменениях состояния, обеспечивает слабую связанность.
- Когда уместно: UI-события, реактивная обработка данных, распределённые события внутри приложения, подписка на изменение модели (MVC). Пример: обновление нескольких видов представления при изменении модели.
- Когда вредно: массовые подписки могут привести к утечкам памяти (подписчики не отписаны), сложно отлаживать цепочки уведомлений (неопределённый порядок), может вызвать каскадные эффекты и гонки в многопоточности. Альтернатива: явные колбэки, синхронные вызовы, stream/Reactive frameworks с управлением жизненным циклом.
4) Adapter
- Что это: приводит интерфейс существующего класса к требуемому интерфейсу клиента (обёртка).
- Когда уместно: интеграция с внешними библиотеками/существующим кодом, когда нельзя/не хочется менять исходный интерфейс. Пример: адаптер между старым API сервисов и новым интерфейсом приложения.
- Когда вредно: когда можно просто рефакторить клиент или библиотеку — адаптер маскирует архитектурную проблему; злоупотребление адаптерами создаёт много уровней проксирования и производит накладные расходы/сложность. Альтернатива: изменение общего интерфейса, фасад при объединении нескольких интерфейсов.
5) Singleton
- Что это: гарантирует единственный экземпляр класса и глобальную точку доступа.
- Когда уместно: реальные глобальные ресурсы с явно одной сущностью — системный логгер, конфигурация приложения, пул подключений (но лучше управлять через контейнер). Короткий пример: readonly конфигурация, доступная везде.
- Когда вредно: приводит к глобальному состоянию, скрытым зависимостям, затрудняет модульное тестирование и параллельность; проблемы с ленивой инициализацией и потокобезопасностью; для большинства случаев лучше использовать DI, явно передавать зависимости или управлять жизненным циклом экземпляра через контейнер.
Краткое резюме: паттерны полезны для решения повторяющихся архитектурных задач, но вредны, когда применяются механически там, где проще и понятнее обойтись без них — выбирать их стоит исходя из потребности в расширяемости, замене реализаций и контроле жизненного цикла объектов.