Найти тему
Сделай игру

Как хороший код становится плохим и обратно

Оглавление

Проблема "плохого кода", по большей части - надуманная. И это не попытка обелить недотёп, а, всего лишь, грустная констатация факта. В каждом проекте всё начинается довольно мило: проработка задачи, выбор правильной архитектуры. А потом начинается хаос, неразбериха, срочные задачи, которые нужны вчера, новый глава команды с новыми требованиями и всё медленно скатывается в хаос.

Все думают, что я работаю, в то время как я...
Все думают, что я работаю, в то время как я...

Проще говоря, проблемы, с которыми мы встречаемся в чужом коде обусловлены, зачастую, не низкой подготовкой предыдущих разработчиков, а такими факторами, как "своё видение изящности кода" (ну, у вас же есть такое) и "принятие неизбежного зла срочных правок".

С вершин до дна

Обычно, этот путь бывает слишком быстрым: любой мало-мальски нуждающийся в поддержке проект всегда стартовал с проработки задачи, так что процесс падения - вклад многих его участников. Впрочем, у него есть некоторое количество вполне объективные признаки:

  • Библиотека функций, выполнение которых сильно зависит от контекста, но не связанные друг с другом;
  • Невероятно длинные файлы с кодом;
  • Функции и классы, привязанные к одной функциональности, но объявленные "вразнобой";
  • Чрезмерно простая или чрезмерно глубокая и сложная структура организации файлов;
  • Прочие причины, о которых я обязательно напишу потом - пока про самое очевидное.

Библиотека, которая приняла почти всё

Это знаете, когда в проекте появляется такая папочка (или файл) utils, в которую сливаются все функции, которые вроде нужны, но хранить их вместе с основным кодом как-то не хочется. Тут будет и локаль-независимая функция сортировки массивов строк, и система приведения даты к утверждённому внутреннему стандарту и хак-включение в приватный цикл фреймворка через не задокументированную функцию обратного вызова: короче говоря всё то, что используется 1 раз (иногда чаще), но держать вместе с основным кодом кажется неуместным. Во-первых, метод большой и пожирает слишком много места на экране. Во-вторых, вдруг ещё раз понадобится.

Потом код программы меняется, а эта самая библиотека утилит, как древняя черепаха, растёт всё больше и больше, таская из версии в версию огромное количество "отложений". И вроде надо бы вычистить этот мусор (который, к тому же, даже не задокументирован), но кто этим заниматься будет? А вдруг ещё что-то сломается?

Это примерно то, с чем сталкиваются, наверное, любые проекты, которые прожили чуть больше года. И с почти 100% вероятностью те, которые существуют на рынке более 10 лет.

Ну, определив проблему - давайте её решать:

  • Все новые, потенциально-библиотечные функции, лучше хранить рядом с кодом, который их использует (в той же папке, с указанием префикса имени и тому подобное): и основной код на омрачает своей многословностью, и всегда рядом, если надо прибить за ненадобностью, и не пухнут и без того многострадальные "утилиты".
  • Если метод (класс, компонент; нужное - подчеркнуть) требуется более чем в одном месте - взвесить ещё раз: а так ли необходимо его делать библиотечным? Довольно частым является случай, когда метод обслуживает несколько видов запросов, но каждый - по разному (в зависимости от контекста в данных или параметров): на такой случай лучше для каждого случая продублировать код: польза поначалу может быть не очевидным, но в процессе развития проекта всё может сильно измениться. Принцип единой ответственности (моё любимое, но так поздно признанное, S из SOLID).
  • Если же такой метод уже в несчастных утилитах - привет рефакторинг. Вполне может оказаться, что метод используется всего один раз и можно всё немного изменить в соответствии со своими потребностями.

С этим разобрались. Ещё раз: двигаем код поближе к месту использования, если используется в разных местах с разным контекстом - дублируем код, но а если уж прямо вообще никак: один и тот же метод используется много где - ну так для того утилиты и существуют, чтобы набиваться подобным кодом.

Сверхдлинные файлы

Для каждой IDE есть некоторый предел строк в файле, по достижении которых она начинает тормозить. Где-то побольше, где-то поменьше. На худой конец можно и (NEO)VI(M) подключить - он, кажется, вообще весь жёсткий диск может за раз отобразить и не поперхнуться. Но, всё же, с таким файлом невозможно продуктивно работать.

Тут, конечно, очень много зависит от правил языка программирования, но, плюс-минус, все современные средства разработки позволяют подключать (импортировать) функциональность. А это значит, ничего не мешает взять текущий файл, выковырять из него часть кода и положить в файлике рядом: и главный файл уменьшится, и какая-то изолированная функциональность получит своё собственное пристанище.

Единственным сложным моментом может стать контекст. Как правило, в больших файлах могут использоваться структуры или переменные, имеющие признак глобальности для всех внутренних методов. Но это, к счастью, довольно легко решается: файл с вынесенной функциональностью оформляется (смешно использовать это слово тут) так, чтобы вызов метода требовало необходимого контекста с возможностью его изменения.

Сложно получилось? Попробую проще, на примере.

Скажем, есть функция Х, которая зависит от глобальных переменных a и b. Глобальные переменные превращаются в структуру, ссылка на которую передаётся в обёртку функции Х; сама функция Х меняется и работает уже не с глобальными переменными, а с глобальной структурой, свойства которой - эти самые переменные. Хотя тут придётся делать, скорее всего, рефакторинг основного файла. Есть и другие способы, например, через функцию обратного вызова, которая обновляет глобальные переменные уже внутри изначального файла.

Подытожим: часть функциональности, выбранной по какому-то обобщающему признаку, переносим в соседний файл; возможно, потребуется что-то придумать, чтобы учитывать контекст. В идеале - это когда содержимое файла на экран помещается; везде свои рекомендации по количеству строк, но старые терминалы отображали, кажется 24 строки. На них можете и ориентироваться.

А, да, ещё они отображали не более 80 символов в длину. Так что если ориентируетесь на 24 строки - то эти строки не должны быть длиннее 80 символов.

Порядок имеет значение

Например, в JS есть функциональность подъёма, это когда функция или переменная используется ещё до того, как была объявлена (есть некоторые исключения, но не суть). А ещё можно ссылаться на свойства глобального объекта (global, window) без упоминания оного. И как, скажите пожалуйста, тут понять - к чему идёт обращение? Да никак, не нужно этого понимать, просто наслаждайтесь тайнописью предыдущего разработчика.

Но насладившись лучше отсортировать функции в порядке их применения. И желательно так, чтобы на экране был виден не только участок кода использующий функцию, но и само объявление функции. Это не всегда возможно, но всегда желательно. Так код начинает читаться сильно лучше.

Файлы, которые обманули чёрта

Бывают такие случаи, когда файлы хранятся в папочке, а внутри подпапочка, а там ещё... короче, чтобы увидеть все файлы приходится растягивать панель дерева файлов где-то на половину экрана.

Такое бывает не часто, но многочисленные вложения сильно сбивают с толку. То есть даже простой способ найти нужный файл приводит к трудностям. Конечно, можно использовать встроенные средства IDE, однако рассчитывать на него в таких вопросах не хотелось бы. А хотелось бы делать всё настолько хорошо, чтобы всё искалось, при необходимости, и само.

Тут, кстати, подходы весьма разнятся. Есть те, кто предлагают свалить все файлы в одну папку (IDE-евангелисты). Мой опыт показал, что это крайней неудобно оказывается в какой-то момент.

Другой подход (и я к нему склоняюсь в большей степени) заключается в том, что есть примерный предел вложений. Скажем, первый уровень - уровень компонента, второй уровень - уровень класса, третий уровень - уровень метода и четвёртый уровень - уровень "на всякий случай". Компоненты могут вкладываться один в другой, но в таком случае, хорошо бы их перемещать в отдельную папочку - "компоненты".

Однако, чаще всего, получается немного сложнее: вложенные узконаправленные компоненты или подклассы, которые размещаются рядом. Но тут подходит правило 5 плюс-минус 2 (лучше минус, чем плюс): предельным количество вложений должно стать число 7. Должно хватить.

Дополнить этот подход я хотел бы правилами в именовании файлов: не буду предлагать никакого "правильного" подхода. Просто выберите тот, который сочтёте правильным. Если файл, то это, например, может быть что-то вроде имя_компонента.имя_класса или имя_компонента.имя_библиотеки.название_метода. Короче от большего к меньшему. При сортировке файлов они окажутся рядом, что уже хорошо. Но в целом, конечно, удобней, когда какой-то достаточно крупный компонент (да и даже компонент, содержащий более 1 файла) имеет свою папочку с соответствующим названием.

Заключение

Думаю, на сегодня достаточно банальностей, которые, почему-то, не так, чтобы часто принимаются во внимание (хотя, возможно, это просто мне так везёт, потому что на каждом новом проекте приходилось их плавно внедрять; резко - коллеги не готовы). Итого:

  1. Храним псевдо-библиотечные методы рядом с местом использования; иногда ради этого можно пойти на дублирование кода.
  2. Длинные файлы разбиваем на несколько файлов покороче, но не забываем про контекст выполнение вытаскиваемых методов.
  3. Пишем функции в порядке, обратному порядку использования: сперва метод, потом его применение. Вообще шик, когда использованный метод и его текст видны одновременно на экране.
  4. Организовываем файлы так, чтобы не было бесконечных вложений или сверх-длинных имён. 5 ± 2 уровней вложенности должно хватить.