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

Еще один момент… Я знаю, что вы не пытаетесь воссоздать «Композиторское программное обеспечение», но стоит ли рассматривать, как Array.reverse() на самом деле изменяет переданный массив, а также возвращает ссылку на него? Мутация! Мутация!

Это не похоже на другие функции JS, которые просто возвращают что-то новое.

Какие последствия это имеет для конвейера или композиции?

Спасибо, Грег Белл! И вопрос, и то, что он подразумевает, заставили меня задуматься. И с этим…

Сказка

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

Да, я имею в виду Златовласку и трех медведей. По сюжету семейство медведей отправилось на прогулку, чтобы дать время еде остыть. Во время прогулки Златовласка входит в их дом и пробует еду, стулья и постельные принадлежности каждого — и в процессе съедает или ломает вещи каждого.

Если вы выполните поиск «Какова мораль Златовласки и трех медведей», вы можете найти это:

Мораль этой истории заключается в необходимости уважать частную жизнь и собственность других и то, как ваши действия причиняют другим вред. В заключение, история Златовласка и три медведя иллюстрирует необходимость уважать частную жизнь и собственность других. …
(Взято из PEDIAA)

Итак, это мораль, и неплохая, но я подозреваю, что рассказчиком этой истории был разработчик.

Другая мораль

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

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

Мы делаем несколько разных вещей здесь.

  1. Мы помещаем переменную myName в таблицы поиска текущей области выполнения;
  2. Мы помещаем строку "Toby" куда-то в память;
  3. Мы «привязываем» эту переменную к этому значению;
  4. С помощью const мы сообщаем этой переменной, что ее нельзя перепрограммировать.

Итак, у нас есть два разных момента, на которые следует обратить внимание:

  • Во-первых, примитивные типы неизменяемы. Вы не можете изменить их на месте. Если бы мы toUpperCase() использовали эту строку, у нас был бы новый экземпляр в новой ячейке памяти. Оригинал изменить нельзя.
  • Далее, const можно объявить только при инициализации. С этого момента эта ссылка неизменна. Таким образом, не только значение не может быть изменено, переменная не может быть изменена.

Это означает, что мы можем доверять тому, что myName останется именно таким, как мы определили. Каждый раз, когда я звоню по номеру myName, я получаю одно и то же значение. По сути, мы поместили его в дом и заперли дом.

Рассмотрим другой случай:

Мы сделали то же самое здесь. myFriends теперь равно const, поэтому всегда будет указывать на один и тот же массив. Все здорово, прекрасно и замечательно... пока мы не сделаем это:

Итак, мы поместили этот массив в переменную, const не меньше... но затем мы отсортировали этот массив. И Array.prototype.sort — один из тех надоедливых методов массива на месте. Мы мутировали массив myFriends.

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

Проблема

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

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

Но что, если два из этих компонентов используют массив Friends? Скажем, один из них позволяет мне добавлять заметки об этих друзьях, а другой может искать их контактную информацию. Представьте, что мы загружаем модуль администратора AddNotesAboutFriends, создаем массив и даже имеем общий стиль объекта между другими вещами, обращающимися к Friends, что позволяет получить дополнительные сведения. Отлично работает, загружает все записи о наших друзьях и позволяет нам добавлять, редактировать и просматривать сделанные нами заметки. Большой!

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

Но… что только что произошло? Если наш компонент ViewFriendDetails сортирует этот массив Friends и AddNotesAboutFriends просматривает этот массив? Возможно, мы нарушили доверие. Мы не можем полагаться на массив Friends, потому что что-то вне нашего контроля теперь мутирует эту штуку, оставляя ее в неожиданном и ненадежном состоянии!

Почему неизменность имеет значение

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

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

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

  • splitOn принимает строку для использования в качестве "разделителя" и строку для разделения. Из этого он возвращает массив.
  • joinWith делает обратное: он берет строку для использования в качестве нашего "объединителя", а затем объединяет массив значений в строку.

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

Затем у нас есть функция reverse. Я хотел написать простую оболочку, чтобы можно было просто передать массив и перевернуть его. Вместо того, чтобы звонить array.reverse(), я хотел иметь возможность звонить reverse(array). Но я упустил из виду эти смыслы.

«Эта функция reverse в данном конкретном случае действительно не имеет значения». Мы используем его только для переходных данных в любом случае, поэтому значение в конечном итоге выбрасывается. Так что на самом деле не имеет значения, что array.reverse() не возвращает что-то новое, верно?

Неправильный.

Это важно. Почему? Потому что я не знаю применения своих функций. У меня нет возможности узнать, где эта функция reverse может использоваться в дальнейшем. Это отличная и полезная функция, она может появляться повсюду. Весь смысл концепции «Функционального программирования» в том, что мы можем создавать эти маленькие простые одно- или двухстрочные функции и соединять их между собой. И они будут работать.

Но в данном случае array.reverse() это Златовласка. Мы вернулись к исходной ссылке на массив и изменили ее. Из-за того, как javascript передает значения, исходный массив и массив внутри функции являются общей ссылкой. Они оба просматривают одну и ту же ячейку памяти, и любой из них может ее видоизменять. Ребята, это плохая идея.

Почему?

Ключевым принципом функционального программирования является «чистота». Когда мы говорим о чистоте, мы имеем в виду, что наши функции должны:

  • Учитывая тот же ввод, вернуть тот же вывод и
  • Не вызывать побочных эффектов.

Итак, для этой функции reverse мы каждый раз получаем одно и то же: когда мы передаем массив, возвращаемое значение — это перевернутый массив. Но мы вызвали побочный эффект! Мы изменили исходный массив, а также вернули его.

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

Простые исправления

В этом случае исправить просто: вместо того, чтобы просто инвертировать массив, мы хотим инвертировать копию массива:

В этом случае, когда мы получаем это array, мы сразу же распространяем его в новый массив. Мы больше не ссылаемся на оригинал, поэтому, когда мы array.reverse() работаем над собственной локальной копией. И когда мы возвращаем значение, исходный массив остается нетронутым.

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

Другие ошибки

Есть и другие методы и места, где нам нужно быть бдительными. Вот типичное предупреждение, взятое из MDN:

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

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

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

Мы можем сделать их более надежными, о некоторых из которых я писал в других статьях: вместо использования class и создания открытого объекта используйте функцию Factory и Object.freeze() возвращаемый метод доступа.

Смысл

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

Первоначально опубликовано на https://dev.to 22 февраля 2022 г.

Создавайте компонуемые веб-приложения

Не создавайте веб-монолиты. Используйте Bit для создания и компоновки несвязанных программных компонентов — в ваших любимых фреймворках, таких как React или Node. Создавайте масштабируемые и модульные приложения с мощными и приятными возможностями разработки.

Перенесите свою команду в Bit Cloud, чтобы совместно размещать и совместно работать над компонентами, а также значительно ускорить, масштабировать и стандартизировать разработку в команде. Начните с компонуемых интерфейсов, таких как Design System или Micro Frontends, или исследуйте компонуемый сервер. Попробуйте →

Узнать больше