Код как живой организм: от говнокода к архитектуре
Как страх «плохого кода» мешает развиваться
Когда я только начинал программировать, мне казалось, что хороший код должен выглядеть идеально с первой строчки:
- Чистые функции,
- Правильные паттерны,
- Идеальная архитектура…
Я боялся браться за задачи, потому что думал:
«А вдруг сделаю не по SOLID? А если нарушу DRY? А если опытные разработчики увидят мой код и назовут его говнокодом?»
Результат? Паралич. Вместо кода — пустой файл, бесконечные раздумья и ноль результата.
Пока не осознал простую вещь:
Код — как живой организм. Он рождается слабым, обрастает мускулами и только потом становится сильным.
И это нормально.
Почему первый код — это почти всегда «говнокод» (и это не плохо)
Working solution > Perfect architecture
Твоя задача — не написать шедевр, а решить проблему.
Пример: функция на Clojure
Допустим, нам нужно отфильтровать список чисел, оставив только чётные.
Первый вариант (наивный, «говнокод»):
(defn filter-even [numbers]
(loop [nums numbers result []]
(if (empty? nums)
result
(let [num (first nums)]
(if (even? num)
(recur (rest nums) (conj result num))
(recur (rest nums) result)))))
Что не так?
- Громоздкий
loop/recur
, хотя в Clojure естьfilter
. - Избыточность, ручное управление состоянием.
Но это работает! И это главное.
3. Эволюция кода: как хаос превращается в порядок
Теперь, когда есть работающее решение, можно его улучшать.
Рефакторинг #1: используем filter
(defn filter-even [numbers]
(filter even? numbers))
Уже лучше! Но что, если нужно ещё и удвоить чётные числа?
Рефакторинг #2: добавляем map
(defn process-numbers [numbers]
(->> numbers
(filter even?)
(map #(* % 2))))
Теперь код:
- Лаконичный,
- Читаемый,
- Расширяемый.
Что изменилось?
- Мы не планировали эту архитектуру заранее.
- Она появилась естественно, когда потребовалось добавить новую логику.
4. Почему паттерны и SOLID — это результат, а не старт
Многие думают, что нужно сначала выучить все принципы, а потом писать код.
Но на практике:
- Ты пишешь как получается,
- Сталкиваешься с проблемами,
- И только потом осознаёшь, что твой код — это, например, Стратегия или Фабрика.
Пример: «случайный» паттерн
Допустим, у нас есть:
(defn save-data [data storage-type]
(cond
(= storage-type :db) (save-to-db data)
(= storage-type :file) (save-to-file data)))
Какие проблемы возникают?
1. Трудно масштабировать
Допустим, появился новый тип хранилища — :s3
. Теперь нужно:
- Лезть в исходную функцию.
- Добавлять новую ветку
cond
. - Рисковать сломать существующую логику.
Проблема: Нарушение Open-Closed Principle (код открыт для изменений, но закрыт для модификаций).
2. Жёсткая связанность
Функция save-data
явно зависит от:
- Конкретных ключей (
:db
,:file
). - Конкретных функций (
save-to-db
,save-to-file
).
Что если:
- Ключи поменяются?
- Нужно будет добавить валидацию
data
перед сохранением?
Сложно тестировать
Чтобы протестировать save-data
, нужно:
- Мокать
save-to-db
иsave-to-file
. - Контролировать все ветки
cond
.
Позже мы понимаем, что это Strategy — и выносим логику в отдельные функции:
(defmulti save-data (fn [data storage-type] storage-type))
(defmethod save-data :db [data _] (save-to-db data))
(defmethod save-data :file [data _] (save-to-file data))
Как прийти к этому решению на практике?
- Сначала пишем «лобовой» код через
cond
/case
(это нормально!). - Замечаем, что веток становится слишком много.
- Рефакторим, когда:
- Появляется новый тип хранилища.
- Начинаются проблемы с тестами.
- Код становится неподдерживаемым.
Задача
Есть список пользователей, и нужно:
- Отфильтровать только активных (
:active? true
). - Оставить их имена.
- Отсортировать по алфавиту.
1. Первая мысль: «Сделаю через циклы!» (потому что это «очевидно»)
(defn get-active-usernames [users]
(let [active-users (loop [users users result []]
(if (empty? users)
result
(let [user (first users)]
(if (:active? user)
(recur (rest users) (conj result user))
(recur (rest users) result)))))
usernames (loop [users active-users result []]
(if (empty? users)
result
(recur (rest users) (conj result (:name (first users))))))
sorted-names (sort usernames)]
sorted-names))
Почему так происходит?
- Мозг сразу тянется к императивному подходу («перебрать, проверить, собрать»).
- Страх перед «неизвестными» функциями (
filter
,map
,comp
). - Кажется, что «так надёжнее» (хотя код превращается в лапшу).
2. Момент осознания: «А ведь это можно упростить…»
После того как код работает, появляется мысль:
«Наверное, есть способ лучше» → гуглим/спрашиваем/экспериментируем.
Рефакторинг: шаг за шагом
Шаг 1. Замена ручного фильтра на filter
(defn get-active-usernames [users]
(let [active-users (filter :active? users) ; <- магия keyword как предиката!
usernames (map :name active-users)
sorted-names (sort usernames)]
sorted-names))
Шаг 2. Убираем промежуточные переменные
(defn get-active-usernames [users]
(sort (map :name (filter :active? users))))
Шаг 3. Тред-ласт макрос (->>
) для читаемости
(defn get-active-usernames [users]
(->> users
(filter :active?)
(map :name)
(sort)))
Психологический подтекст
- Страх «незнания»
- Сначала кажется, что
loop/recur
— это «безопасно», потому что понятно. - Функции высшего порядка (
filter
,map
) требуют доверия к языку.
- Эффект «Я это сделал!»
- После рабочего, но громоздкого кода, приходит облегчение.
- Теперь можно сфокусироваться на улучшениях (а не на панике).
- Архитектура через рефакторинг
- Изначально ты не задумываешься о «чистоте» — просто решал задачу.
- Но после рефакторинга код сам стал похож на паттерн (например, Pipeline).
Индустрия программирования создаёт токсичный перфекционизм
Курсы, собеседования, статьи — все твердят:
- «SOLID, паттерны, чистый код!»
- «Если не знаешь DRY/KISS/YAGNI — ты не разработчик!»
- «Архитектура должна быть идеальной с первого коммита».
Результат?
Новички (и даже мидлы) впадают в паралич анализа:
- «Вдруг я сделаю не по стандартам?»
- «А что если мой код назовут говнокодом?»
- «Я не понимаю, как применить Factory Method здесь… Может, не браться за задачу?»
Но правда в том, что:
Лучшие практики — это выводы, а не входные данные.
Их нельзя «выучить». Их можно только прочувствовать через боль.
Почему «быстрый говнокод» — это не стыдно, а необходимо
Пример из жизни
Представь, что ты учишься играть на гитаре:
- Теория: «Вот 100 аккордов, вот 20 гамм, играй как Хендрикс».
- Реальность: Сначала ты бренчишь «Кузнечика» на трёх аккордах — и это нормально.
То же с кодом. Алгоритм эффективного обучения:
- Накидал прототип (пусть криво, но работает).
- Понял задачу (ага, вот где подводные камни).
- Улучшил (рефакторинг, паттерны, оптимизация).
Почему Clojur-разработчикам тут повезло
- ФП + иммутабельность = меньше последствий от хаотичного прототипирования.
- REPL = моментальная проверка гипотез без «перезапуска проекта».
- Парадигма «данные > функции» = легко пересобирать логику.
3. Как бороться со страхом «неидеального кода»
Принять: первый код почти всегда плох
- Linus Torvalds не написал Git «с нуля идеальным».
- Rich Hickey до Clojure делал десятки прототипов.
Твой код — черновик, а не высеченный в мраморе памятник.
Отделяй «эксперимент» от «продакшна»
- Создайте ветку
shitcode-prototype
. - Пиши в ней как угодно, чтобы проверить гипотезу.
- Если идея сработала — тогда рефакторь.
Спрашивай не «Как сделать идеально?», а «Как сделать сейчас?»
- Плохо: «Я не знаю, как применить Command Pattern здесь…»
- Хорошо: «Сначала сделаю через
cond
, потом посмотрю, что можно улучшить».
4. Что говорят практики
Кен Бек (автор TDD):
«Сначала сделайте так, чтобы оно работало. Затем сделайте правильно. И только потом — быстро».
Роберт Мартин (Clean Code):
«Единственный способ написать чистый код — сначала написать грязный, а потом почистить его».
Clojure-разработчик в дикой природе:
«80% моего кода начинаются как
(->> data (map ...) (filter ...) (грубая агрегация))
.
А потом я удаляю половину и получаю хорошее решение».
Вывод: принцип «Сначала практика, потом теория»
- Курсы/книги учат «как должно быть», но не «как дойти до этого».
- Твой опыт важнее чем мнение «экспертов».
- Clojure — идеальный язык, чтобы быстро ваять прототипы и виртуозно рефакторить.
P.S.: Попробуй сегодня:
- Намеренно напиши максимально «плохой» код для задачи.
- Заставь его работать.
- Затем улучши.
- Почувствуй разницу!
Вывод: кайфуй от процесса, а не гонись за идеалом
- Не бойся писать плохой код — это этап, а не приговор.
- Архитектура рождается в процессе — а не в вакууме.
- Паттерны — результат опыта — а не его условие.
Первый вариант кода — это не «плохо», это исследование.
Мы не пишем архитектуру — мы её открываем в процессе.
Сначала — работающий код. Потом — красивый. И только затем — «идеальный» (если он вообще нужен).
Главное — начать.