Источник: https://habr.com/ru/post/424051/


Данная статья является конспектом книги “Чистый Код” Роберта Мартина и моим пониманием того, каким Чистый Код должен быть. Тут нет разделов о тестировании, TDD, о том какая должна быть архитектура и т.д. Здесь все только о том, каким должен быть Чистый Код. Да, возможно, тема Чистого Кода уже заезженна, но тем не менее еще не все с ним знакомы и, тем более, я не встретил аналогов контента, который содержится в моей статье.

Общее

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

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

Всегда задумывайся о том как можно сделать твой код проще, чище и читабельнее.

  • Какие кейсы могут быть у задачи?
  • Все ли я учел?
  • Что может пойти не так?
  • Что можно объединить?
  • Есть ли похожий функционал?
  • Что тут лишнее?
  • Как сделать проще?
  • Как сделать читабельнее?
  • Как сделать понятнее?

Чистый Код

Как писать чистый и хороший код? Это похоже на написание книги. Сначала ты делаешь черновик и потом причесываешь его до того состояния, в котором тебе было бы приятно его читать. Всегда помни, что твой код должен рассказывать историю происходящего, чтобы читатель мог ее понять.

Под сущностью понимается — интерфейс, класс, метод, переменная, объект и т.д.

  • Чистый код простой, выразительный и направлен на конкретную задачу.
  • Чистый код читается легко, как проза. Если это не так, то его стоит рефакторить.
  • Чистый код легко изменять. Он не должен быть жестко завязан на куче сущностей. Любую сущность можно легко изменить.
  • Чистый код намного лучше проходит ревью. Если ревью проходит с огромным количеством комментариев, то он не чистый и его надо рефакторить.
  • Чистый код всегда выглядит так, словно над ним очень долго трудились. Какие бы пути для его улучшения ты не искал, ты все равно придешь к тому, что этот код лучший. Соответственно, чистый код — продуманный до всех мелочей.
  • Правило бойскаута: Оставь место стоянки чище, чем оно было до тебя. Это легко перекладывается и на программирование. Видишь грязный код? Сделай его чище, пока решаешь свою задачу. Не стоит увлекаться этим и если грязный код очень грязный, то стоит выделить отдельную задачу и время для его очистки.
  • Не бойся делать изменений. Если ты хочешь их сделать, то значит у тебя есть на то причины, а значит ты сделаешь код лучше и чище. Тем более тесты покажут нет ли ошибок в твоем коде (при условии, что они вообще есть).
  • Любая сущность должна отвечать за один функционал и только за него. И она должна выполнять его хорошо. Single Responsibility.
  • Если сущность отвечает сразу за два и более действий, то её функционал нужно разделять.
  • Код должен читаться сверху вниз.
  • В хорошей и грамотной архитектуре внесение изменений обходится без значительных затрат и усилий.
  • Удаляй мертвый код. Мертвый код это код, который не будет вызван ни при каких условиях или код, который нигде не используется.

Наименования и разделения

  • Используй понятные и удобнопроизносимые имена для любых сущностей. Они должны описывать почему эта сущность существует, что она делает и как используется.
  • Не бойся тратить время на выбор лучшего и понятного имени. Ты выиграешь в будущем при работе или чтении этого кода.
  • Если название сущности не соответствует еë функционалу или по названию не понятно, что сущность делает, то еë надо переименовать в самое понятное название. Если этого сделать невозможно, то значит с еë функционалом что-то не так и еë надо рефакторить.
  • Сущность, которая имеет в названии “And”, “With” — нарушает Single Responsibility. Функционал такой сущности стоит разделять. Но этим правилом стоит иногда пренебрегать.
  • Непонятные тексты, строки стоит выносить в переменные и давать им понятные названия.
  • Названия методов должны содержать глагол, который описывает, что этот метод делает и ключевое слово с которым работает данный метод. Если в названии метода нет глагола, то эта сущность не должна быть методом или ему нужно дать правильное название.
  • Нужно избегать одинаковых наименований для двух разных целей.
  • Если сущность имеет схожее с другой сущностью название, то скорее всего их функционал очень сильно похож и их нужно объединить? Если нет, то их названия нужно менять так, чтобы они не были похожими.
  • Если ты мысленно переименовываешь сущность, когда читаешь код, чтобы тебе было понятнее понимать её функционал, то переименуй её в это мысленное название.
  • Выбери одно слово для одной концепции. Сложно будет понимать функционал, когда у тебя есть fetch, retrieve и get в названиях. Пусть лучше везде будет get.
  • Длинное и понятное имя лучше, чем короткое, но непонятное.

Функции

  • Функции должны быть короткими и компактными.
  • Функции должны быть очень короткими и очень компактными.
  • Приблизительный максимум 20 строк и 150 символов в одной строке, если не влезает, то нужно разделять.
  • Функция должна выполнять только одну операцию.
    • Она должна выполнять её хорошо и ничего другого она делать не должна.
    • Если функция выполняет только те действия, которые находятся на одном уровне абстракции, то функция выполняет одну операцию.
    • Чтобы определить выполняет ли функция более одной операции, попробуй извлечь из нее другую функцию, которая не будет являться простой переформулировкой реализации.
  • Любые условные операторы с длинными выборами через switch-case, if-else должны разделяться или объединяться без дублирования, возможно на классы с реализациями, а выбор реализации передать базовому классу, фабрике или еще кому-то.
  • If, else, while и т.д. должны содержать вызов одной функции. Так будет читабельнее, понятнее и проще.
  • Идеальное количество входных аргументов для функции = 0. Если входных аргументов больше трех, то стоит задуматься каким образом лучше от них избавиться, например, создать класс для этих аргументов.
  • Чем больше входных аргументов, тем тяжелее понимается функция.
  • Функция в которую передается аргумент-флаг, от которого зависит работа функции говорит о том, что функция выполняет более одной операции. Такие функции следует разбить на две и вызывать их уровнем выше.
  • Функция, которая изменяет входной аргумент, должна отдавать ссылку на измененный объект, а не просто изменять без возврата. String transform(String text)
  • Если функция, должна изменять входной аргумент, то пусть она изменяет состояние своего объекта-владельца.
  • Если входной аргумент функции не должен меняться (и используется дальше в коде), то следует скопировать значение аргумента и внутри функции работать с копией.
  • Вместо return null лучше использовать пустой объект — Collection.empty() или null-объект -EmptyObject().
  • Всегда старайся использовать нестатические функции. Если это невозможно, то используй статические.
  • Если есть код, который должен следовать один за другим, то передавай результаты первой функции во вторую, чтобы кто-нибудь не изменил последовательность вызовов.
  • Используй полиморфизм вместо if/else или switch/case или when.
  • Избегай отрицательных условий.

Комментарии

  • Не используй комментарии, если ты можешь использовать функцию или переменную вместо этого.
  • Не комментируй плохой код — перепиши его. Не стоит объяснять, что происходит в плохом коде, лучше сделать его явным и понятным.
  • Комментарии можно использовать для передачи какой-то информации, предупреждения о последствиях, но не для объяснения того, как работает код.
  • Используй TODO и FIXME в тех случаях, когда нужно пометить, что код нуждается в доработке, но сейчас нет ресурсов на это.
  • Используй //region REGIONNAME //endregion REGIONNAME, а если используешь, то подумай можно ли разделить region на сущности.
  • Документируй код, который является сложным, но чистым.
  • Не оставляй старый закомментированный код. Ты можешь найти его в истории коммитов, если необходимо.
  • Комментарии должны быть краткими и понятными. В комментариях с информацией не должно быть много информации. Все должно быть кратко и по делу.

Форматирование и правила

  • Соблюдай codestyle, принятый на проекте.
  • Соблюдай правила, принятые в команде.
  • При соблюдении форматирования и codestyle код будет читаться проще и лучше. Ведь не зря книгу отдают на редакцию, перед тем, как её издавать.
  • Нужно иметь автоматические средства, которые будут форматировать код за тебя.
  • Файл с исходным кодом должен быть как газетная статья. Есть заголовок, краткое описание в виде параметров и содержание в виде функций. Если это не так, то стоит изменить форматирование.
  • Сущности, связанные друг с другом, должны находиться рядом, например, в одном package, чтобы было проще навигировать по коду.
  • Переменные(поля) класса должны находиться вверху класса.
  • Переменные методов должны находиться ближе к своему месту использования.
  • Функции должны находиться в порядке вызова. Если одна вызывает другую, то вызывающая функция должна находиться над вызываемой. C другой стороны, приватные функции более низкого уровня могут находиться внизу файла и не мешать пониманию кода высокого уровня. Но я предпочитаю первый способ.

Объекты и структуры данных

  • Ты должен работать с абстракциями, чтобы реализацию можно было легко изменить.
  • Ты должен работать с абстракциями, потому что клиент, использующий функционал, не должен знать о деталях реализации, он должен знать какую реализацию в каком случае использовать.
  • Ты должен предоставлять API, с которым стоит работать и скрывать детали реализации, структуру. Так будет проще работать с такими сущностями и добавлять новые виды поведений, функционала и реализаций.
  • DTO — Data Transfer Object. Класс, который содержит только данные и никакого функционала. Нужен для того, чтобы передавать какие-то данные. Объект такого класса должен быть неизменяемым.

Классы

  • Классы должны быть компактными.
  • Классы должны быть еще компактнее.
  • Имя класса должно описывать его ответственности. Отсюда можно и вычислить размер класса.
  • Функционал класса должен четко соответствовать и вписываться в название класса.
  • Разделяй связанность на маленькие классы. Жесткой и обильной связанности не должно быть — это усложняет поддержку и развитие проекта.
  • Помни о Single Responsibility. Сущность должна иметь одну и только одну причину для изменения.
  • Соблюдай инкапсуляцию. Ослабление инкапсуляции всегда должно быть последней мерой.
  • Обычно мы объявляем переменные и вспомогательные функции приватными, но иногда их нужно объявлять protected и иметь возможность обратиться к ней из теста.
  • Если группа функций относится к определенному функционалу, то эту группу функций можно и нужно выделить в отдельный класс и использовать его экземпляр.

Обработка ошибок

  • Используй Exceptions вместо возвращения кодов ошибок.
  • Обработка ошибок — это одна операция. Если в функции есть ключевое слово try, то после блоков catch/finally ничего другого в функции быть не должно.
  • Если у тебя есть enum, который перечисляет ошибки, то от него лучше избавиться и вместо него использовать исключения.
  • Используй unchecked exceptions, чтобы явно указать на место в котором есть проблемы. Такие ошибки не нужно отлавливать, вместо этого нужно написать код так, чтобы этой ошибки никогда не было.
  • Передавай достаточное количество информации вместе с выбросом исключения, чтобы потом пользователи твоего кода могли понять, что же действительно произошло.
  • Вместо условных операторов с обработкой ошибок лучше выбрасывать исключения и обрабатывать их.
  • Не передавай null куда-либо. Старайся этого максимально избежать.
  • Обработка ошибок — это отдельная задача и не относится к основной логике программы.

Границы

  • Мы всегда используем какие-либо библиотеки, которые чаще всего дают нам слишком широкий, слишком маленький функционал или конфликтуют с ожидаемым функционалом, что делает код грязнее в его конечном использовании. Избежать этого можно просто применив паттерны типа Decorator, Adapter, Facade или другие.
  • Бывают ситуации, когда тебе нужно работать с функционалом, который находится в разработке или пока что не адаптирован для использования в продакшен коде. В этом случае стоит представить чего ты ждешь от библиотеки/этого функционала и написать свой интерфейс или создать сущность с которыми ты будешь работать в своем проекте так, как тебе нужно. Когда библиотека доделается и станет стабильной, ты адаптируешь её под свои готовые структуры и использовать уже готовый функционал.

Послесловие

В данной статье представлены лишь рекомендации к написанию Чистого Кода. Разумеется, ими можно пренебрегать. Вам лишь стоит понять, что у любого вашего решения должны быть аргументы в пользу него.