В сфере разработки программного обеспечения существует своеобразный культ, который я наблюдаю уже несколько лет. Его приверженцы собираются на ревью кода, в Slack-каналах и на конференциях, повторяя свою священную мантру: «Всё должно быть просто». Они используют простоту как святыню, отвергая всё, что хоть немного сложно, как «избыточную инженерию», и ведут отрасль к краху, чувствуя себя при этом морально выше других.
Не поймите меня неправильно — я не противник простоты. Но я глубоко настороженно отношусь к любому догматизму, а современная религиозная вера в «простой код» достигла уровня, который заставил бы средневековых монахов показаться прагматиками.
Парадокс простоты
Позвольте мне начать с того, что, вероятно, расстроит некоторых людей: простота переоценена как абсолютная ценность. Одержимость отрасли этим создала ложную дихотомию, в которой мы делаем вид, что сложные системы по своей природе плохи, а простые автоматически хороши. Это примерно так же полезно, как утверждение, что все короткие книги лучше длинных.
Реальная проблема заключается в том, что мы смешали несколько разных концепций и объединили их под общим термином «простота». Мы говорим о простом коде, простой архитектуре, простых интерфейсах и простых функциях, как будто это всё одно и то же. Это не так.
Рассмотрим следующее: функция, которая кажется простой в реализации, часто проста только в данный момент. Она становится сложной, когда вы пытаетесь её поддерживать, расширять или интегрировать с другими частями вашей системы. Простой интерфейс, который скрывает сложную внутреннюю структуру, может быть простым в использовании, но сложным в создании и поддержке. Это не одно и то же.
Четвёртый закон, который никто не соблюдает
В дизайне программного обеспечения есть элегантный принцип, который заслуживает гораздо большего внимания, чем ему уделяется: поддерживаемость системы обратно пропорциональна сложности её отдельных частей. Позвольте этому осесть на мгновение.
Обратите внимание, чего он не говорит. Он не говорит, что поддерживаемость обратно пропорциональна общей сложности системы. Он не говорит, что вы должны делать всё смехотворно простым. Он конкретно говорит об отдельных частях.
Это различие имеет решающее значение, и большинство разработчиков его полностью упускают. У вас может быть сложная, функциональная система, которую легко поддерживать — если вы разбили её на простые, понятные компоненты. И наоборот, у вас может быть кодовая база, которая кажется простой на первый взгляд, но с ней сложно работать, потому что отдельные части плохо абстрагированы.
Подумайте об этом так: строительство небоскрёба путём слияния всего в три огромных нестандартных стальных луча — это не то же самое, что строительство из стандартизированных балок. Да, подход с тремя балками выглядит «проще» под определёнными углами. Пока один луч не треснет, и вы не поймёте, что загнали себя в архитектурный тупик.
Где чрезмерная простота терпит неудачу
Существует опасное заблуждение, что сокращение сложности системы до абсолютного минимума всегда правильный ход. Но вот неудобная правда: чрезмерная простота ведёт к неадекватным решениям. Когда вы без разбора избавляетесь от сложности, вы часто лишаетесь функциональности, гибкости и устойчивости.
Позвольте привести вам практический пример. Допустим, вы создаёте систему электронной коммерции, и кто-то настаивает на том, чтобы схема базы данных была «простой». Поэтому вы создаёте одну таблицу products с json столбцом для всех данных вариантов. Готово. Просто. Чисто. Красиво.
Затем меняются требования. Вам нужно фильтровать продукты по размеру и цвету. Ваш подход с JSON-блобами внезапно становится обузой. Вы теперь делаете неэффективные сканирования всей таблицы, вы не можете добавить правильные индексы, ваши запросы либо невероятно медленные, либо кошмарные в написании. Вы оптимизировали простоту на этапе проектирования и заплатили за это сложностью на всех остальных этапах.
Ловушка «что, если» против ловушки чрезмерной простоты
В разработке существует реальное явление, которое я называю «простота Шрёдингера» — когда разработчики разрываются между двумя тревогами. С одной стороны, есть тревога «что, если»: «Что, если мы добавим эту абстракцию, что, если мы подготовимся к этой функции, что, если мы структурируем это таким образом?» Это ведёт к избыточной инженерии.
С другой стороны, есть тревога простоты: «Что, если мы уберём слишком много? Что, если мы не сможем расширить это позже?» Это ведёт к недостаточной инженерии.
Нынешняя атмосфера в обществе серьёзно наказывает за избыточную инженерию (и это правильно — YAGNI реален), но она создала скрытое принятие недостаточной инженерии. Вас редко критикуют на ревью кода за то, что вы сделали что-то слишком простым. Но я видел множество систем, повергнутых на колени архитектурными решениями, принятыми под лозунгом простоты.
Решение не в том, чтобы качнуть маятник в другую сторону. Оно в том, чтобы понять, что сложность зависит от контекста. Некоторые проблемы требуют сложных решений. Другие — нет. Делать вид, что все они требуют одного и того же уровня простоты, — это интеллектуальная нечестность, завёрнутая в демонстрацию смирения.
Практическая рамка: разделение проблем
Вместо того чтобы догматично настаивать на простоте везде, давайте подумаем, где простота действительно важна, а где нет.
отдельных компонентов"] F -->|Нет| H["Стремиться к радикальной простоте"] G --> I["Реализация с
ясными абстракциями"] H --> J["Реализация прямого решения"] I --> K["Модульный, тестируемый код"] J --> L["Минимальные зависимости"]
Вот рамка, которую я нахожу полезной:
Упрощайте безжалостно на границах. Ваши публичные API, пользовательские интерфейсы, форматы конфигурации — они должны быть максимально простыми. Это то место, где стоимость сложности оплачивается пользователями, и обычно стоит усилий скрыть сложность за простыми интерфейсами.
Допускайте соответствующую сложность во внутренностях. Если вашей внутренней реализации нужны сложные шаблоны, структуры данных или абстракции для обработки реального домена проблемы, это совершенно нормально. Ваши пользователи этого не видят. Ваша команда это понимает. Это не избыточная инженерия; это правильная инженерия.
Разделяйте интеграционную сложность от основной сложности. Система может быть внутренне согласованной и элегантной, используя сложные библиотеки и фреймворки. Важно, чтобы точки интеграции были хорошо управляемыми. Вам не нужно изобретать всё заново, чтобы сохранить простоту.
Реальная стоимость чрезмерной простоты
Позвольте мне показать вам конкретный пример, где совет «держите это просто» ведёт нас в заблуждение.
Предположим, вы создаёте систему аутентификации, и кто-то предлагает: «Просто используйте текстовые пароли и сравнивайте их напрямую. Просто!»
# «Простой» подход — НЕ ИСПОЛЬЗОВАТЬ В ПРОИЗВОДСТВЕ
def authenticate_user(username: str, password: str):
user = db.find_user(username)
if user and user.password == password:
return True
return False
Это объективно просто. Это также объективно небезопасно, неэффективно и нарушает несколько лучших практик безопасности. «Простое» решение неадекватно.
Действительно подходящее решение более сложное:
import hashlib
import secrets
from cryptography.fernet import Fernet
import time
class AuthenticationService:
def __init__(self, secret_key: str):
self.cipher = Fernet(secret_key.encode())
def hash_password(self, password: str, salt: str = None) -> tuple[str, str]:
"""Хэшировать пароль с солью, используя PBKDF2 для безопасности."""
if salt is None:
salt = secrets.token_hex(32)
# Использовать PBKDF2 вместо простого хэширования
hash_obj = hashlib.pbkdf2_hmac(
'sha256',
password.encode(),
salt.encode(),
100000 # итераций
)
return hash_obj.hex(), salt
def authenticate_user(self, username: str, password: str) -> bool:
"""Аутентифицировать пользователя с защитой от атак по времени."""
user = db.find_user(username)
# Всегда выполнять вычисления, чтобы избежать атак по времени
start_time = time.time()
if user:
stored_hash, stored_salt = user.password_hash, user.salt
provided_hash, _ = self.hash_password(password, stored_salt)
# Использовать сравнение за постоянное время
result = secrets.compare_digest(provided_hash, stored_hash)
else:
# Всё равно делать фиктивные вычисления, чтобы избежать перечисления пользователей
self.hash_password(password)
result = False
# Добавить преднамеренную задержку для предотвращения атак по времени
elapsed = time.time() - start_time
if elapsed < 0.5:
time.sleep(0
