View on GitHub

wiki

Technical Excellence Wiki

Чистый код (Clean Code)

Что такое Чистый Код?

Чистый Код (Clean Code) - это код, который просто читать и просто изменять.

Определение было введено Робертом Мартином в начала 2000-х и описано в его одноимённой книге. Оно появилось, как противоположность плохому или “грязному” кода.

Характеристики Чистого Кода:

Зачем писать Чистый Код?

Мартин Фаулер так оценивает качество кода:

Любой дурак может написать код, понятный компьютеру. Хороший разработчик пишет код, понятный человеку.

По мере увеличения количества строк кода в системе она, как правило, утрачивает способность так же быстро меняться, как это было на старте проекта. Следовательно, кривая производительности неуклонно снижается. Заказчикам приходится отказываться от многих фич, потому что они не смогут их представить рынку в нужное время.

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

Чтобы код оставался действительно “мягким” (software) и мог эволюционировать вместе с требованиями к нему, а не превращался в что-то жёсткое и плохо пахнущее, существуют, в том числе практики по написанию Чистого Кода.

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

Элегантный код является фундаментом для элегантной архитектуры. Чистая архитектура начинается не с презентаций или диаграмм, а с покрытых тестами модулей, которые легко поддаются изменению и готовы к любым требованиям. Правильный дизайн системы, как и код конкретного метода, являются растущим садом, который легко превращается в поля из сорняков без должного ухода. Подробнее о культуре “выращивания” можно почитать в статье EmergentArchitecture

Да и красивый, простой, чистый код писать и развивать гораздо приятнее. Вряд ли кто-то хочет другого. Не так ли? ;)

Рекомендации

Наименование классов, методов, переменных очень важно при понимании поведения кода. Сравните два примера:

Пример 1:

for (int i = 0; i < list.size(); i++) {
    for (int j = 0; j < list[i].size(); j++) {
        check(i, j);
        // ...
    }
}

Пример 2:

for (int rowNumber = 0; rowNumber < rowsOfseatsInCinemaHall.size(); rowNumber++) {
    for (int seatNumber = 0; seatNumber < rowsOfseatsInCinemaHall[rowNumber].size(); seatNumber++) {
        checkIfEmpty(rowNumber, seatNumber);
        // ...
    }
}

Думаю, многие согласятся, что Пример 1 может быть из любой предметной области — это просто обход двумерного списка с какой-то логикой. А вот из Примера 2 очевидно, что речь идёт о переборе мест в зале кинотеатра, чтобы проверить, какое из них не занято. Согласны?

Ниже собраны несколько рекомендаций (но далеко не всех), который могут вам помочь.

Передавайте через удобопроизносимые имена свои намерения

Раньше во многих системах длина названия переменной или поля таблицы в СУБД была сильно ограничена. Сейчас, например, в Java длина имени переменной может составлять 65535 символов. Так можно назвать переменную “срок кредита в месяцах”:

var m = ...;

Но лучше написать полностью:

var loanTermInMonth = ...;

Избегайте кодирования типа в имени

Современные среды разработки могут цветом отразить ту информацию, которую раньше приходилось “зашивать” в имена:

interface IAdapter {...}

String sName = ...
float fRate = ...

Не стоит добавлять, например, к интерфейсам “I”, а к именам переменных первую букву их типа:

interface Adapter {...}

String name = ...
float rate = ...

Избегайте отрицаний

Логику с “не” понять сложнее, тем более если она по ходу инвертируется несколько раз. Так делать не стоит:

boolean isNotWeekend = ...

Так выглядит проще:

boolean isWeekday = ...

Используйте термины предметной области

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

Соблюдайте орфографию, чтобы упростить поиск в будущем

Многие современные среды разработки проверяют орфографию названия переменных, методов, классов. Не игнорируйте эти предупреждения. Несколько лет назад я потратил несколько часов на поиск ошибки, которая заключалась в первой кириллической букве “с” в названии поля createDate. Думаю, многие сталкивались с подобным: chek вместо check или tset вместо test.

Меняйте имена, если они перестают быть актуальными

Если в процессе разработки название, например класса, утратило актуальность, то переименуйте его. Метод say класса Dog не должен возвращать "Mew!".

Выбирайте такие названия, чтобы классы и аргументы функций были существительными, а функции — глаголами или глагольными словосочетаниями

Сигнатура метода подсчёта площади прямоугольника может выглядеть так:

class Calculate {
    public int areaOfRectangle(int x1, int x2) {
        ...
    }
}

А лучше написать так:

class Rectangle { 
    public int calculateArea(int width, int height) {
        ...
    }
}

Используйте минимально возможное количество аргументов метода/функции

Известный пример, который может продемонстрировать важность этой рекомендации. Сигнатура метода assertEquals в 4-ой версии популярного фреймворка JUnit была следующей:

public static void assertEquals(String message, String expected, String actual)

А в 5-ой версии:

public static void assertEquals(String expected, String actual, String message)

Ошибок компиляции нет, тесты стали красными. Требуется больше времени, чтобы найти причину. Идеальное количество аргументов равно 0, так сложнее всего ошибиться при вызове. Если вам нужно передать больше 2-3 аргументов, используйте объект обёртку:

class ConditionWrapper {
    private String message;
    private String expected;
    private String actual;
    ...
}

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

Избегайте выходных аргументов

Например, в PL/SQL без них нельзя обойтись. Но в ООП-языках принят ритм течения кода сквозь функцию или метод от передачи аргументов к возвращаемому значению. Обратно может сбивать и запутывать.

Поменять цвет прямоугольника можно так, где result - результат выполнения метода, который нужно будет обработать далее:

class Rectangle {
    public void setColor(Color color, boolean result) {...}
}

Но лучше использовать метод с указанием возвращаемого типа:

class Rectangle {
    public boolean setColor(Color color) {...}
}    

Разделяйте команды и запросы

Пример: Методы вида getX только дают информацию, не меняя её, методы setX наоборот только изменяют.

Подробнее о принципе Разделения команд и запросов.

Применяйте компактные методы

Раньше была распространена рекомендация, что тело функции или метода может быть прочитано полностью без использования прокрутки, т.е. помещаться на целиком на мониторе. На современных мониторах с разрешением FullHD и выше такое правило уже не работает. Но рекомендация остаётся актуальной — методы больше 20 строк разбивайте на несколько более мелких. Более компактные методы проще читать и тестировать.

Избегайте нескольких операций в рамках одного метода

У метода должен быть только один источник требований. Если в рамках одного метода выполнить несколько операций, то возрастёт его сложность, частота изменений и количество тестов, которыми его придётся покрыть.

Избегайте нескольких уровней абстракции

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

public void draRectangle(int width, int height) {
    board.line(0, 0, width, 0); 
    board.line(width, 0, width, height); 
    board.line(width, height, 0, height); 
    board.line(0, height, 0, 0); 
    renderOnGPU(board); // обращение к видеокарте, лучше вынести этот вызов
}

Выносите дублирующие шаги в отдельные блоки

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

Избегайте конструкции switch…case

Громоздкая конструкция switch…case отъест заметный объём строк кода, делая часто простые преобразования. Если в вашем случае происходит соотнесение одних объектов другим (например, String positionName -> Integer positionCode), то можно использовать карту (map). Если пример сложнее, то рекомендуется использовать один из нескольких вариантов. Я часто использовал шаблоны Команда и Абстрактная Фабрика до появления Java 8, теперь фокус в сторону функциональных интерфейсов.

Используйте исключения вместо возвращения кодов ошибок

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

public int countLength(String name) {
    if (name == null) {
        return -1;
    } 
    return name.length();
}

Как можно было бы переписать код, убрав оператор ветвления и добавив собственное исключение NullNameException:

public int countLength(String name) throws NullNameException  {
    return Optinal.ofNullable(name).orElseThrow(new NullNameException());
}

Избегайте цепочки вызовов методов

Возможно “крушение поезда”, если, например, один из методов в цепочке вернёт null:

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath()

Как можно было написать:

Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();

Безусловно, если, например, переменная opts будет равна null, то любой из фрагментов кода выбросит исключение. Но во втором случае сообщение об ошибки будет иметь номер конкретной строки, и такое поведение проще обработать, чем в первом случае.

Также стоит отметить, что такая сцепка может не иметь таких побочных последствий, например, при использовании шаблона Builder мы уверены, что любой из методов в цепочке null не вернёт:

MyObject.builder().propetyOne(...).propetyTwo(...)...propetyN(...).build();

Или в частности при использовании Stream API:

Stream.of([1,2,3).map((i) -> i*i).filter((i) -> i > 1)...

Используйте принципы и паттерны проектирования

Классы, спроектированные с учётом принципов SOLID и шаблонов проектирования, имеют меньше шансов “замусориться”. Как правило, у опытного разработчика есть навык чтения и распознавания таких структур в коде, что позволяет тратить меньше времени на понимание такого кода. К тому же эти принципы и паттерны прошли эволюционный путь в несколько десятков лет и языков программирования, поэтому кажется, что ещё остаётся класс задач, решение которых они могут упростить.

Начинайте разработку с модульных тестов

Начать с тестов поможет статья о TDD. Как писать тесты вы можете подробнее узнать из статей xUnit и Модульные тестах.

Избегайте вредных комментариев

В общем случае наличие комментариев с деталями реализации указывает на то, что код не может быть прочитан и понят сразу. Это является запахом “грязного” кода. Такие комментарии часто устаревают и вводят в заблуждениях всех, кто пытается разобраться с этим кодом.

Известная цитата Рона Джеффриса описывает такие комментарии:

Код никогда не врёт в отличие от комментариев.

“Закомментированные” функции, методы или их части также не имеет смысла при использовании VCS (Git, SVN, …), когда код можно восстановить на любую дату в прошлом в пару кликов мыши. То же относиться и к логу изменений, указанию прошлых редакторов — всю эту информацию можно делегировать к системе контроля версий.

Используйте полезные комментарии

Но есть ряд случаев, когда комментарии могут быть полезны:

Как сделать существующий код “чище”?

Помните, что перед экспериментами с кодом, даже на первый взгляд очень очевидными, было бы полезно покрыть этот код тестами. Это поможет вам сэкономить много времени и нервов! А в статье о Рефакторинге можно найти несколько способов, как безопасно улучшить свой существующий код.

Видео

Статьи

Книги

Адаптировал: Кротов Артём.

Остались вопросы? Задавай в нашем чате.