Данная статья является конспектом книги "Чистый Код" Роберта Мартина и моим пониманием того, каким Чистый Код должен быть. Тут нет разделов о тестировании, 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 или другие.
- Бывают ситуации, когда тебе нужно работать с функционалом, который находится в разработке или пока что не адаптирован для использования в продакшен коде. В этом случае стоит представить чего ты ждешь от библиотеки/этого функционала и написать свой интерфейс или создать сущность с которыми ты будешь работать в своем проекте так, как тебе нужно. Когда библиотека доделается и станет стабильной, ты адаптируешь её под свои готовые структуры и использовать уже готовый функционал.
Послесловие
В данной статье представлены лишь рекомендации к написанию Чистого Кода. Разумеется, ими можно пренебрегать. Вам лишь стоит понять, что у любого вашего решения должны быть аргументы в пользу него.