Java Script

Часть 1, Часть 2, Часть 3

Изменяемое состояние

Photo by Alexey Turenkov on Unsplash

Что такое состояние? Говоря простым языком, состояние — это любые временные данные, хранящиеся в памяти. Например, это могут быть переменные или поля внутри объектов. Само по себе состояние вполне безобидно, но изменяемое состояние является одним из самых больших источников проблем в ПО, особенно в сочетании с ООП.

Ограниченные возможности человеческого мозга

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

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

Проблемы с изменяемым состоянием

Давайте рассмотрим на практике, как изменяемость создает проблемы в коде:

const increasePrice = (item, increaseBy) => {
  // никогда так делайте
  item.price += increaseBy;

  return item;
};

const oldItem = { price: 10 };

const newItem = increasePrice(oldItem, 3);

// печатает newItem.price 13
console.log('newItem.price', newItem.price);

// печатает oldItem.price 13
// не ожидали?
console.log('oldItem.price', oldItem.price);

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

Как этого избежать? Созданием и возвратом нового объекта:

const increasePrice = (item, increaseBy) => ({
  ...item,
  price: item.price + increaseBy
});

const oldItem = { price: 10 };

const newItem = increasePrice(oldItem, 3);

// печатает newItem.price 13
console.log('newItem.price', newItem.price);

// печатает oldItem.price 10
// все ок!
console.log('oldItem.price', oldItem.price);

Имейте в виду, что копирование с использованием оператора ES6 spread  создает поверхностную, а не полную копию. Это означает, что в копии не будет вложенных свойств. Например, если сверху у item было свойство item.seller.id, seller нового item все равно будет ссылаться на старый item. Для работы с неизменяемым состоянием используйте более надежные альтернативы, вроде immutable.js и Ramda lenses.

Предлагаемая конфигурация ESLint:

rules:
  fp/no-mutation: warn
  no-param-reassign: warn

Не прибегайте к методу push с массивами

Те же самые проблемы возникают при изменении массивов, если вы используете такой метод, как push:

const a = ['apple', 'orange'];
const b = a;

a.push('microsoft')

// ['apple', 'orange', 'microsoft']
console.log(a);

// ['apple', 'orange', 'microsoft']
// не ожидали?
console.log(b);

А вы думали, что массив b не изменится? Этой ошибки можно было бы легко избежать, создав новый массив вместо вызова push:

const newArray = [...a, 'microsoft'];

Недетерминизм 

Недетерминизм — этот странный термин обозначает неспособность программ выводить одни и те же выходные данные при одних и тех же входных данных. Вы можете сказать, что 2+2==4, но это не всегда так с недетерминированными программами. Да, в большинстве случаев 2+2==4, но иногда результат может быть равен 3, 5 и даже 1004.

Изменяемое состояние по своей природе недетерминировано и подвергает код недетерминизму (как показано выше). Ирония в том, что недетерминизм повсеместно считается нежелательным в программировании, однако наиболее популярные парадигмы программирования (ООП и императивное программирование) по своей сути недетерминированы из-за своей зависимости от изменяемого состояния.

Неизменяемость 

Если изменяемость — не самый лучший вариант, что выбрать? Конечно, неизменяемость! Использование неизменяемости считается хорошей практикой и многими поощряется. Некоторые могут не согласиться с моим мнением и сказать, что изменяемое состояние — это круто (фанаты Rust?). Я могу ответить только одно — изменяемость только вредит кодовой базе и делает ее менее надежной.

Предлагаемая конфигурация ESLint:

rules:
  fp/no-mutating-assign: warn
  fp/no-mutating-methods: warn
  fp/no-mutation: warn

Избегайте использования let

Я никого не удивлю, сказав, что var ни в коем случае нельзя использовать для объявления переменных в JavaScript. Однако многие удивятся, узнав, что ключевое слово let тоже нужно избегать. Переменные, объявленные с помощью let могут быть переназначены, что затруднит понимание кода. 

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

Что можно использовать вместо let? Конечно же ключевое слово const! const не гарантирует неизменяемость, но запрещает переназначение, что улучшает понимание кода. И, честно говоря, вам не нужен let — в большинстве случаев код, который переназначает переменные, можно извлечь в отдельную функцию. Давайте рассмотрим пример:

let discount;

if (isLoggedIn) {
  if (cartTotal > 100  && !isFriday) {
    discount = 30;
  } else if (!isValuedCustomer) {
    discount = 20;
  } else {
    discount = 10;
  }
} else {
  discount = 0;

И тот же пример, извлечённый в функцию:

const getDiscount = ({isLoggedIn, cartTotal, isValuedCustomer}) => {
  if (!isLoggedIn) {
    return 0;
  }

  if (cartTotal > 100  && !isFriday()) {
    return 30;
  }
  
  if (!isValuedCustomer) {
    return 20;
  }
  
  return 10;
}

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

Предлагаемая конфигурация ESLint:

rules:
  fp/no-let: warn

Читайте также:


Перевод статьи Ilya Suzdalnitski: Here’s How Not to Suck at JavaScript