🧑‍💻 Код
August 6

Код как живой организм: от говнокода к архитектуре

Как страх «плохого кода» мешает развиваться

Когда я только начинал программировать, мне казалось, что хороший код должен выглядеть идеально с первой строчки:

  • Чистые функции,
  • Правильные паттерны,
  • Идеальная архитектура...

Я боялся браться за задачи, потому что думал:
«А вдруг сделаю не по 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))))

Теперь код:

  • Лаконичный,
  • Читаемый,
  • Расширяемый.

Что изменилось?

  1. Мы не планировали эту архитектуру заранее.
  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))

Как прийти к этому решению на практике?

  1. Сначала пишем «лобовой» код через cond/case (это нормально!).
  2. Замечаем, что веток становится слишком много.
  3. Рефакторим, когда:
    • Появляется новый тип хранилища.
    • Начинаются проблемы с тестами.
    • Код становится неподдерживаемым.

Задача

Есть список пользователей, и нужно:

  1. Отфильтровать только активных (:active? true).
  2. Оставить их имена.
  3. Отсортировать по алфавиту.

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)))

Психологический подтекст

  1. Страх «незнания»
    • Сначала кажется, что loop/recur — это «безопасно», потому что понятно.
    • Функции высшего порядка (filter, map) требуют доверия к языку.
  2. Эффект «Я это сделал!»
    • После рабочего, но громоздкого кода, приходит облегчение.
    • Теперь можно сфокусироваться на улучшениях (а не на панике).
  3. Архитектура через рефакторинг
    • Изначально ты не задумываешься о «чистоте» — просто решал задачу.
    • Но после рефакторинга код сам стал похож на паттерн (например, Pipeline).

Индустрия программирования создаёт токсичный перфекционизм

Курсы, собеседования, статьи — все твердят:

  • «SOLID, паттерны, чистый код!»
  • «Если не знаешь DRY/KISS/YAGNI — ты не разработчик!»
  • «Архитектура должна быть идеальной с первого коммита».

Результат?
Новички (и даже мидлы) впадают в паралич анализа:

  • «Вдруг я сделаю не по стандартам?»
  • «А что если мой код назовут говнокодом?»
  • «Я не понимаю, как применить Factory Method здесь… Может, не браться за задачу?»

Но правда в том, что:

Лучшие практики — это выводы, а не входные данные.
Их нельзя «выучить». Их можно только прочувствовать через боль.

Почему «быстрый говнокод» — это не стыдно, а необходимо

Пример из жизни

Представь, что ты учишься играть на гитаре:

  • Теория: «Вот 100 аккордов, вот 20 гамм, играй как Хендрикс».
  • Реальность: Сначала ты бренчишь «Кузнечика» на трёх аккордах — и это нормально.

То же с кодом. Алгоритм эффективного обучения:

  1. Накидал прототип (пусть криво, но работает).
  2. Понял задачу (ага, вот где подводные камни).
  3. Улучшил (рефакторинг, паттерны, оптимизация).

Почему 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.: Попробуй сегодня:

  1. Намеренно напиши максимально «плохой» код для задачи.
  2. Заставь его работать.
  3. Затем улучши.
  4. Почувствуй разницу!

Вывод: кайфуй от процесса, а не гонись за идеалом

  • Не бойся писать плохой код — это этап, а не приговор.
  • Архитектура рождается в процессе — а не в вакууме.
  • Паттерны — результат опыта — а не его условие.

Первый вариант кода — это не «плохо», это исследование.
Мы не пишем архитектуру — мы её открываем в процессе.
Сначала — работающий код. Потом — красивый. И только затем — «идеальный» (если он вообще нужен).

Главное — начать.