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

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

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

Вступление

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

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

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

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

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

Используйте аргументы по умолчанию вместо коротких цепочек или условных операторов

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

Этот пример проиллюстрирован в следующих фрагментах кода.

function setName(name) {
 const newName = name || ‘Juan Palomo’;
}
function setName(name = ‘Juan Palomo’) {
 // …
}

Функциональные аргументы (два или меньше, в идеале)

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

Эта рекомендация очень важна, потому что, хотя мы в нее не верим, когда у нас много аргументов, обычно несколько группируются вместе, составляя объект. Мы должны отказаться от использования примитивов (таких как строка, число, логическое значение и т. Д.) И начать использовать объекты, которые находятся на более высоком уровне абстракции. Фактически, мы были бы ближе к бизнес-логике и все дальше от нижнего уровня.

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

Значительным улучшением является использование такого предмета, как бургер, для создания нового гамбургера. Таким образом, мы сгруппировали атрибуты в один объект (в данном случае это будет плоский объект без прототипа).

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

function newBurger(name, price, ingredients, vegan) {
 // …
}
function newBurger(burger) {
 // …
}
function newBurger({ name, price, ingredients, vegan }) {
 // …
} 
const burger = {
  name: ‘Chicken’,
  price: 1.25,
  ingredients: [‘chicken’],
  vegan: false,
};
newBurger(burger);

Избегайте побочных эффектов - глобальные переменные

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

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

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

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

let fruits = ‘Banana Apple’;
function splitFruits() {
 fruits = fruits.split(‘ ‘);
}
splitFruits();
console.log(fruits); // [‘Banana’, ‘Apple’];
function splitFruits(fruits) {
 return fruits.split(‘ ‘);
}
 
const fruits = ‘Banana Apple’;
const newFruits = splitFruits(fruits);
 
console.log(fruits); // ‘Banana Apple’;
console.log(newFruits); // [‘Banana’, ‘Apple’];

Избегайте побочных эффектов - изменяемые объекты

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

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

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

В JavaScript методы, которые работают со структурой данных массива, делятся на те, которые вносят изменения в объекты, и те, которые этого не делают. Например, операции push, pop или sort работают с одной и той же структурой данных, в то время как операции filter, reduce или map генерируют новые структуры данных и не изменяют основную.

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

Я приведу вам два наглядных примера, в которых один вносит изменения в структуру данных тележки, а другой не вносит таких изменений.

const addItemToCart = (cart, item) => {
  cart.push({ item, date: Date.now() });
}; 
const addItemToCart = (cart, item) => {
  return […cart, {
    item, 
    date: Date.now(),
  }];
};

Функции должны делать одно

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

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

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

function emailCustomers(customers) {
  customers.forEach((customer) => {
    const customerRecord = database.find(customer);
    if (customerRecord.isActive()) {
      email(client);
    }
   });
}

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

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

function emailActiveCustomers(customers) {
  customers
   .filter(isActiveCustomer)
   .forEach(email);
 }
 
function isActiveCustomer(customer) {
  const customerRecord = database.find(customer);
  return customerRecord.isActive();
}

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

Функции должны быть только одним уровнем абстракции

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

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

function parseBetterJSAlternative(code) {
  const REGEXES = [
    // …
  ];
 
 const statements = code.split(‘ ‘);
 const tokens = [];
 REGEXES.forEach((REGEX) => {
   statements.forEach((statement) => {
     // …
   });
 });
 
 const ast = [];
 tokens.forEach((token) => {
   // lex…
 });
 
 ast.forEach((node) => {
   // parse…
 });
} 
```

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

const REGEXES = [ // …];
function tokenize(code) { 
  const statements = code.split(‘ ‘);
  const tokens = [];
  REGEXES.forEach((REGEX) => {
    statements.forEach((statement) => {
      tokens.push( /* … */ );
    });
  });
 return tokens;
}
function lexer(tokens) {
 const ast = [];
 tokens.forEach((token) => ast.push( /* */ ));
 return ast;
}
function parseBetterJSAlternative(code) {
 const tokens = tokenize(code);
 const ast = lexer(tokens);
 ast.forEach((node) => // parse…);
}

Отдайте предпочтение функциональному программированию императивному программированию

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

Я рекомендую прочитать Блог Элвина Александра и, в частности, пост, в котором он описывает преимущества функционального программирования.

Ниже я резюмирую основные преимущества использования функционального программирования в императиве.

1. Чистые функции легче рассуждать.
2. Тестировать проще, а чистые функции хорошо поддаются таким методам, как тестирование на основе свойств.
3. Отладка проще.
4. Программы более надежны.
5. Программы написаны на более высоком уровне и, следовательно, их легче понять.
6. Сигнатуры функций более значимы.
7. Параллельное / параллельное программирование Полегче.

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

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

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

Обратите внимание на код, в котором сделан простой счетчик. Вы должны помнить о нескольких переменных: total, i, items, items.length, price; в то время как в функциональной реализации у нас было бы только: total, price и items. Если вы привыкли к функциональным операторам, чтение будет довольно быстрым и понятным.

const items = [{
   name: ‘Coffe’,
   price: 500
 }, {
   name: ‘Ham’,
   price: 1500
 }, {
   name: ‘Bread’,
   price: 150
 }, {
   name: ‘Donuts’,
   price: 1000
 }
];
let total = 0;
for (let i = 0; i < items.length; i++) {
 total += items[i].price;
}
const total = items
 .map(({ price }) => price)
 .reduce((total, price) => total + price);

Использовать цепочку методов

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

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

В нашем конкретном случае мы должны построить нечто подобное, используя объекты или функции. В следующих примерах мы проиллюстрируем класс Car, в котором используются связанные методы вместо традиционных.

class Car {
 constructor({ make, model, color } = car) {
   /* */
 }
 
 setMake(make) {
   this.make = make;
 }
 setModel(model) {
   this.model = model;
 }
 setColor(color) {
   this.color = color;
 }
 save() {
   console.log(this.make, this.model, this.color);
 }
} 
const car = new Car(‘WV’,’Jetta’,’gray’);
car.setColor(‘red’);
car.save();
class Car {
 constructor({ make, model, color } = car){}
 
 setMake(make) {
   this.make = make;
   return this;
 }
 setModel(model) {
   this.model = model;
   return this;
 }
 setColor(color) {
   this.color = color;
   return this;
 }
 save() {
   console.log(this.make, this.model, this.color);
   return this;
 }
}
const car = new Car(‘WV’,’Jetta’,’gray’)
.setColor(‘red’)
.save();

Заключение

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

Дизайн функций, использующих чистый код, важен, потому что функции являются основным элементом, разделяющим код.

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

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

Наконец, мы рассмотрели следующие моменты:

  • Используйте аргументы по умолчанию вместо короткого замыкания или условных выражений
  • Аргументы функции (в идеале два или меньше)
  • Избегайте побочных эффектов - глобальные переменные
  • Избегайте побочных эффектов - изменяемые объекты
  • Функции должны делать одно
  • Функции должны быть только одного уровня абстракции
  • Отдайте предпочтение функциональному программированию над императивным программированием