Вы знаете это чувство, когда вы приходите на buffet и наполняете свою тарелку всем, что доступно, а потом понимаете, что вам стоило ограничиться только пиццей? Примерно то же самое происходит, когда разработчики обнаруживают шаблоны проектирования.
Не поймите меня неправильно — я люблю шаблоны проектирования. Они как хорошо организованный набор инструментов для решения повторяющихся проблем. Но есть неудобная правда, которую никто не хочет признавать на технических конференциях: шаблоны проектирования стали изолентой современной разработки программного обеспечения. Мы используем их повсюду, а потом удивляемся, почему наши «элегантные решения» оказываются сложными.
Неудобная правда о поклонении шаблонам
Годы я верил в евангелие «Банды четырёх». Я читал о Singleton, Factory, Observer, Strategy — обо всех 23 канонических шаблонах — и думал: «Наконец-то, ключи к хорошему проектированию программного обеспечения!» Чего я не понимал, так это того, что я учился видеть проблемы через призму решений, которые уже запомнил. Классический случай, когда молоток смотрит на мир и видит только гвозди.
Настоящая проблема не в самих шаблонах проектирования. Проблема в наших отношениях с ними. Мы вознесли их до уровня религиозной доктрины, хотя на самом деле они всего лишь документированные методы решения проблем. Проблемы начинаются, когда мы забываем это важное слово: проблемы.
Парадокс шаблонов: доступность как ловушка
Вот парадокс, который не даёт мне спать по ночам: шаблоны проектирования были созданы, чтобы сделать разработку программного обеспечения более доступной для обычных разработчиков. И они блестяще справились с этой задачей. Теперь разработчики, которые иначе не смогли бы создать сложные системы, могут это сделать.
Но потом происходит нечто странное.
Мы наблюдаем, как неопытные команды берут такие шаблоны, как Singleton, и создают архитектурные кошмары. Я однажды работал над кодовой базой с сотнями Singleton. Не двадцатью. Сотнями. Разработчик, который создал её, явно знал шаблон достаточно хорошо, чтобы понять, что он полезен… просто не достаточно хорошо, чтобы понять, когда остановиться.
Асимметрия жестока: реализация шаблона проектирования занимает три минуты. Удаление его занимает три дня, когда вы наконец понимаете, что это была ошибка. И к тому времени вся система зависит от него.
Когда гибкость становится хрупкостью
Давайте поговорим о том, как выглядит сложность на самом деле. Вот классический пример того, что я называю «ползучестью шаблонов»:
# Простое решение, которое на самом деле работает
class UserRepository:
def __init__(self, database):
self.database = database
def find_by_id(self, user_id):
return self.database.query(f"SELECT * FROM users WHERE id = {user_id}")
def save(self, user):
self.database.execute(f"INSERT INTO users VALUES (...)")
# «Гибкое» решение с использованием шаблонов
class DatabaseConnection:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
class RepositoryFactory:
@staticmethod
def create_user_repository():
return UserRepository(DatabaseConnection())
class UserRepositoryAdapter:
def __init__(self, repository):
self._repository = repository
self._cache = {}
self._observers = []
def register_observer(self, observer):
self._observers.append(observer)
def find_by_id(self, user_id):
if user_id in self._cache:
return self._cache[user_id]
user = self._repository.find_by_id(user_id)
self._cache[user_id] = user
self._notify_observers('user_found', user)
return user
def _notify_observers(self, event, data):
for observer in self._observers:
observer.update(event, data)
class CachingStrategy:
def execute(self, repository, user_id):
return repository.find_by_id(user_id)
Оба делают одно и то же. Один — восемь строк. Один — пятьдесят. Угадайте, какой из них разработчик чувствовал себя более гордо, отправляя в продакшн?
Первый решает проблему. Второй решает проблему и три гипотетических проблемы, которые так и не возникли.
Скрытые затраты: когнитивная нагрузка
Есть кое-что, о чём никто не говорит, когда обсуждают шаблоны проектирования: они умножают когнитивную нагрузку на всю вашу команду.
Каждому новому разработчику, который присоединяется к вашему проекту, теперь нужно изучить не только вашу бизнес-логику, не только ваш фреймворк, но и каждый шаблон, который ваша кодовая база накопила за годы. Если ваша команда использовала Factories, Adapters, Decorators и Strategies — все законные шаблоны! — вы только что добавили часы к их онбордингу.
А ещё есть кошмар с code review. Использовал ли разработчик правильный шаблон? Использовал ли он его правильно? Должен ли был это быть Adapter или Facade? Теперь вы обсуждаете мелочи проектирования вместо того, чтобы обсуждать реальную проблему.
Убийца творчества
Вот что меня пугает в кодовых базах с большим количеством шаблонов: они подавляют творческое решение проблем.
Когда у вас есть молоток, сделанный из шаблонов «Банды четырёх», каждая проблема начинает выглядеть так, будто вам нужен ещё один молоток. Особенно новички попадают в эту ловушку. Они изучают пять шаблонов и пытаются использовать все пять в своей следующей функции. В результате получается «проектирование, управляемое шаблонами», а не «проектирование, управляемое проблемами».
Я наблюдал, как разработчики переписывали простое условное выражение в State pattern только потому, что узнали о нём на прошлой неделе. Код увеличился с десяти строк до сотни. Функциональность осталась идентичной. «Гибкость» так и не была использована.
Рассмотрим этот сценарий:
// Что нужно коду
const processUser = (user) => {
if (user.status === 'active') {
sendNotification(user);
} else if (user.status === 'pending') {
sendApprovalEmail(user);
} else if (user.status === 'inactive') {
archiveUser(user);
}
};
// Что кто-то неизбежно предложит
class UserState {
execute(user) { throw new Error('Not implemented'); }
}
class ActiveUserState extends UserState {
execute(user) { return sendNotification(user); }
}
class PendingUserState extends UserState {
execute(user) { return sendApprovalEmail(user); }
}
class InactiveUserState extends UserState {
execute(user) { return archiveUser(user); }
}
class UserProcessor {
constructor() {
this.states = {
active: new ActiveUserState(),
pending: new PendingUserState(),
inactive: new InactiveUserState()
};
}
process(user) {
return this.states[user.status].execute(user);
}
}
Вторая реализация «элегантна». Она также решает проблему, которой у вас нет. У вас четыре статуса пользователей. Может быть, добавится ещё пять. А может и нет. Зачем добавлять семнадцать файлов и сто строк кода для «может быть»?
Когда шаблоны имеют смысл
Я не выступаю за хаос в коде. Некоторые контексты действительно выигрывают от использования шаблонов проектирования.
Ситуация A: Вы создаёте библиотеку UI, где плагины могут подключаться к нескольким событиям жизненного цикла → Наблюдатель (Observer) имеет смысл
Ситуация B: У вас есть несколько способов создать сложные подключения к базе данных → Фабрика (Factory) имеет реальную ценность
Ситуация C: Вам нужно поменять реализации во время выполнения → Стратегия (Strategy) решает реальную потребность
Ситуация D: Возможно, вам понадобится поддерживать несколько бэкендов базы данных в будущем → Может быть, стоит реализовать уровень абстракции
Ситуация E: У вас простая операция CRUD, которая сейчас работает нормально → Опустите шаблон
Ключевое слово во всех законных случаях: реальная ценность. Не теоретическая ценность. Не гипотетическая ценность. Не «в книге сказано, что мы должны».
Антипаттерн паттернов
Есть нечто мрачно забавное в том, что мы начали относиться к «простоте» как к шаблону проектирования самому по себе. «Не повторяйся», «YAGNI» (You Aren’t Gonna Need It), «Keep It Simple, Stupid» — все эти принципы, связанные с шаблонами, якобы защищают нас от злоупотребления шаблонами.
Но знаете что? Их часто игнорируют так же легко. YAGNI становится принципом, о котором люди упоминают на стендапе, добавляя Factory для класса, который инстанцируется ровно один раз.
Вот фреймворк для принятия решений, который я на самом деле использую сейчас, и я обнаружил, что он отсеивает шум:
которая появляется несколько раз?"] A -->|Нет| B["Напишите простой код"] A -->|Да| C["Решал ли я это
одинаково дважды?"] C -->|Нет| B C -->|Да| D["Вероятно ли, что эта проблема
в
