Пишем чистый и читаемый код: руководство для начинающих разработчиков

0
2267
views

Перевод статьи «The junior developer’s guide to writing super clean and readable code».

Чистый и читаемый код

Уметь писать код это одно, а уметь писать чистый код – совсем другое. Но что такое «чистый код»? Я создал это короткое руководство как раз для того, чтобы помочь вам разобраться в этом вопросе и овладеть искусством написания чистого кода.

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

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

Ваш код должен читаться, как хорошая статья. Можете считать свои классы/файлы заголовками, методы – абзацами, а строки – предложениями. Вот несколько характеристик чистого кода:

  1. Чистый код сфокусирован: каждая функция, класс и модуль должны делать какое-то одно действие, но делать его хорошо.
  2. Он должен быть элегантным. Чистый код прост для чтения. Когда вы читаете такой код, у вас должна непроизвольно появляться улыбка. У вас должна быть уверенность, что вы точно знаете, что именно делает этот код.
  3. Чистый код это заботливо написанный код. Кто-то потратил время, чтобы сделать его простым и упорядоченным. Этот человек уделил соответствующее внимание деталям. Он явно был неравнодушен к своему делу.
  4. У вас не должно возникать проблем с тестами: чистый код не может быть сломанным!

Переходим к главному вопросу: как же разработчику-джуниору научиться писать такой код? Предлагаю воспользоваться следующими советами.

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

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

Чтобы сделать код понятным и легким для чтения, следите за тем, чтобы отступы, переносы строк и форматирование были последовательными (одинаковыми в рамках одного файла/проекта). Вот примеры хорошего и плохого кода:

Хорошо

function getStudents(id) { 
if (id !== null) {
go_and_get_the_student();
} else {
abort_mission();
}
}
  • Лишь взглянув на код, вы видите, что там внутри функции есть блок if/else.
  • Благодаря последовательности в отступах и расположении скобок легко видеть, где начинаются и кончаются блоки кода.
  • Скобки располагаются последовательно. Обратите внимание, что открывающие скобки function и if расположены одинаково.

Плохо

function getStudents(id) {
if (id !== null) {
go_and_get_the_student();}
else
{
abort_mission();
}
}

Вах! А вот здесь много неправильных вещей.

  • Беспорядочные отступы – невозможно сказать, где кончается функция или где начинается блок if/else (да, здесь он есть!).
  • Скобки перемешаны и непоследовательны.
  • Нелогичная разбивка кода на блоки с помощью межстрочного интервала.

Конечно, в нашем примере все преувеличено, зато хорошо демонстрирует преимущества последовательности в отступах и форматировании. Не знаю, как вам, а мне первый пример читать гораздо легче!

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

Чистка кода

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

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

function changeStudentLabelText(studentId){                  
const studentNameLabel = getStudentName(studentId);
}
function getStudentName(studentId){
const student = api.getStudentById(studentId);
return student.name;
}

Этот отрывок кода хорош по многим причинам:

  • Функции и аргументы имеют понятные, хорошие имена. Когда разработчик читает этот код, у него в голове сам собой выстраивается текст: «Если я вызову метод getStudentName() со studentId, я получу имя студента».
  • Внутри метода getStudentName() переменные и вызовы метода также имеют понятные имена. Нам несложно понять, что метод вызывает API, принимает объект student и возвращает свойство name.

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

  • Выберите определенный стиль нейминга и придерживайтесь его. То есть, нужно использовать camelCase или under_scores, но не то и другое одновременно!
  • В именах функций, методов и переменных должно отражаться то, что они делают, или то, чем они являются. Если ваш метод что-то принимает, вставьте в имя «get». Если ваша переменная хранит цвет машины, назовите ее, например, carColour.

Бонусный совет: если вы не можете придумать имя для своей функции или метода, есть вероятность, что эта функция делает слишком много всего. Разбейте ее на более мелкие! Например, если ваша функция (исходя из того, что она делает) должна называться updateCarAndSave(), создайте два разных метода: updateCar() и saveCar().

При необходимости используйте комментарии

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

Документирующие комментарии это комментарии, которые описывают, что делает определенная функция или класс. Если вы пишете библиотеку, такие комментарии пригодятся разработчикам, которые будут ее использовать. Вот пример из useJSDoc:

/** * Solves equations of the form a * x = b 
@example *
// returns 2 * globalNS.method1(5, 10);
@example *
// returns 3 * globalNS.method(5, 15);
@returns {Number} Returns the value of x for the equation. */ globalNS.method1 = function (a, b) { return b / a; };

Поясняющие комментарии делаются для всех (в том числе и для самого автора в будущем), кому придется заниматься поддержкой, рефакторингом или расширением кода. Часто такие комментарии опускаются в угоду «самодокументированному коду». Вот пример поясняющего комментария:

/* This function calls a third party API. Due to some issue with the API vender, the response returns "BAD REQUEST" at times. If it does, we need to retry */ 
function getImageLinks(){
const imageLinks = makeApiCall();
if(imageLinks === null){
retryApiCall();
} else {
doSomeOtherStuff();
}
}

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

Просто лишние и бесполезные комментарии:

// эта функция устанавливает возраст студента 
function setStudentAge();

Запутывающие комментарии:

// эта функция устанавливает полное имя студента 
function setLastName();

Забавные или обидные комментарии:

// этот метод имеет 5000 строк, рефакторинг невозможен, так что даже не пытайся 
function reallyLongFunction();

Помните о принципе DRY (Don’t Repeat Yourself – «Не повторяйтесь»)

Принцип DRY

Принцип DRY звучит так:

«Каждая часть знания должна иметь единственное, непротиворечивое и авторитетное представление в рамках системы».

На самом простом уровне это означает, что следует уменьшать количество дубликатов в коде. (Заметьте, я сказал «уменьшать», а не «полностью исключать», потому что в некоторых случаях дублирующийся код это не конец света!).

Дублирующийся код может стать кошмаром в плане поддержки и внесения изменений. Рассмотрим пример.

Допустим, вы на заказ создаете веб-приложение для отдела кадров. Это приложение позволяет админам через API добавлять в базу данных пользователей с указанием их ролей. Есть три вида ролей: сотрудник, менеджер и админ. Давайте рассмотрим несколько функций, которые могут быть при этом:

function addEmployee(){ 
    // create the user object and give the role
    const user = {
        firstName: 'Rory',
        lastName: 'Millar',
        role: 'Admin'
    }
    
    // add the new user to the database - and log out the response or error
    axios.post('/user', user)
      .then(function (response) {
        console.log(response);
      })
      .catch(function (error) {
        console.log(error);
      });
}

function addManager(){  
    // create the user object and give the role
    const user = {
        firstName: 'James',
        lastName: 'Marley',
        role: 'Admin'
    }
    // add the new user to the database - and log out the response or error
    axios.post('/user', user)
      .then(function (response) {
        console.log(response);
      })
      .catch(function (error) {
        console.log(error);
      });
}

function addAdmin(){    
    // create the user object and give the role
    const user = {
        firstName: 'Gary',
        lastName: 'Judge',
        role: 'Admin'
    }
    
    // add the new user to the database - and log out the response or error
    axios.post('/user', user)
      .then(function (response) {
        console.log(response);
      })
      .catch(function (error) {
        console.log(error);
      });
}

Класс! Все работает и жизнь прекрасна. Но спустя некоторое время клиент возвращается и говорит: «Здравствуйте! Мы бы хотели, чтобы сообщение об ошибке содержало фразу «Произошла ошибка». А также, чтобы вам жизнь сахаром не казалась, мы хотим изменить конечную точку API с /user на /users. Спасибо!»

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

Итак, что если нам изменить этот код, чтобы он стал более сфокусированным? Посмотрите на следующий вариант:

function addEmployee(){ 
    // create the user object and give the role
    const user = {
        firstName: 'Rory',
        lastName: 'Millar',
        role: 'Admin'
    }
    
    // add the new user to the database - and log out the response or error
    saveUserToDatabase(user);
}

function addManager(){  
    // create the user object and give the role
    const user = {
        firstName: 'James',
        lastName: 'Marley',
        role: 'Admin'
    }
    // add the new user to the database - and log out the response or error
    saveUserToDatabase(user);
}

function addAdmin(){    
    // create the user object and give the role
    const user = {
        firstName: 'Gary',
        lastName: 'Judge',
        role: 'Admin'
    }
    
    // add the new user to the database - and log out the response or error
    saveUserToDatabase(user);
}

function saveUserToDatabase(user){
    axios.post('/users', user)
      .then(function (response) {
        console.log(response);
      })
      .catch(function (error) {
        console.log("there was an error " + error);
  });
}

Мы перенесли логику, создающую вызов API, в ее собственный метод saveUserToDatabase(user) (хорошее ли это имя? Вам решать!). Этот метод будут вызывать другие методы, чтобы сохранить пользователя. Теперь, если нам снова нужно будет изменить логику API, нам придется внести правки лишь в один метод. Более того, если нам нужно будет добавить другой метод, создающий пользователей, у нас уже будет готовый метод для сохранения пользователя в базе данных через API. Ура!

Пример рефакторинга с применением полученных знаний

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

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

function addNumbers(number1, number2)
{
    const result = number1 + number2;
        const output = 'The result is ' + result;
        console.log(output);
}

// this function substracts 2 numbers
function substractNumbers(number1, number2){
    
    //store the result in a variable called result
    const result = number1 - number2;
    const output = 'The result is ' + result;
    console.log(output);
}

function doStuffWithNumbers(number1, number2){
    const result = number1 * number2;
    const output = 'The result is ' + result;
    console.log(output);
}

function divideNumbers(x, y){
    const result = number1 / number2;
    const output = 'The result is ' + result;
    console.log(output);
}

Нашли?

  • Отступы непоследовательны. Не важно, какой именно формат отступов мы используем, главное, чтобы он сохранялся во всем коде.
  • Вторая функция имеет ненужные комментарии. Прочитав имя функции и код внутри нее, мы и без комментариев понимаем, что она делает.
  • В третьей и четвертой функции хромает нейминг. doStuffWithNumbers() это не лучшее имя для функции, к тому же, оно не поясняет, что именно делает эта функция. (x, y) тоже не слишком описательны. Что они из себя представляют? Это функции? Числа? Бананы?
  • Методы делают больше одного действия. Они осуществляют вычисления, но также и отображают результаты. Мы можем выделить логику отображения результатов в отдельный метод, следуя принципу DRY.

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

function addNumbers(number1, number2){
	const result = number1 + number2;
	displayOutput(result)
}

function substractNumbers(number1, number2){
	const result = number1 - number2;
	displayOutput(result)
}

function multiplyNumbers(number1, number2){
	const result = number1 * number2;
	displayOutput(result)
}

function divideNumbers(number1, number2){
	const result = number1 * number2;
	displayOutput(result)
}

function displayOutput(result){
	const output = 'The result is ' + result;
	console.log(output);
}
  • Мы исправили непоследовательность отступов.
  • Улучшили нейминг функций и переменных.
  • Удалили избыточные комментарии.
  • Перенесли логику displayOutput() в ее собственный метод. Таким образом, если нам нужно будет изменить вывод, нам придется вносить изменения только в одном месте.

Поздравляю! Теперь вы можете гордо заявлять на собеседованиях и в своем резюме, что знакомы с принципами написания чистого кода!

Не перестарайтесь с «чисткой» кода

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

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

ОСТАВЬТЕ ОТВЕТ

Please enter your comment!
Please enter your name here