Создание приложения-разделителя: сколько дел можно разделить на одну операцию?

Создание приложения-разделителя: сколько дел можно разделить на одну операцию?

9 марта 2023 г.

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

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

Истории пользователей

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

Интерфейс

Можно предположить, что пользователи или наше приложение будут знакомы с этой нотацией:

Image description

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

Реализация

Я создал простую реализацию приложения. Он доступен здесь и выглядит следующим образом:

Image description

Тело index.html определяет наше представление:

<input type="number" id="numerator" placeholder="numerator" />

<hr />

<input type="number" id="denominator" placeholder="denominator" />

<br />

<button id="equals">=</button>

<div id="result"></div>

а логика в main.js:

const numerator = document.querySelector("#numerator"),
  denominator = document.querySelector("#denominator"),
  equals = document.querySelector("#equals"),
  result = document.querySelector("#result");

equals.addEventListener("click", () => {
  const numeratorValue = parseFloat(numerator.value),
    denominatorValue = parseFloat(denominator.value);

  const resultValue = numeratorValue / denominatorValue;

  result.innerHTML = resultValue;
});

Ошибки

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

Представление числа с плавающей запятой

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

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

Например, деление 0,3 на 0,1 дает неожиданный результат в JavaScript:

>> 0.3/0.1
2.9999999999999996

Тот же результат будет отображаться для пользователей нашего приложения:

Image description

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

Слепое округление вводит новый пограничный случай: если числитель достаточно мал, в какой-то момент результат будет округлен до 0.

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

Максимальное безопасное целое число

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

В какой-то момент JavaScript не может обеспечить для нас надежность целочисленных значений — достаточно велико впечатление, что два разных целых числа представлены одинаково.

JavaScript сохраняет значение края как:

>> Number.MAX_SAFE_INTEGER
9007199254740991

С этого момента наши расчеты становятся ненадежными, как вы можете видеть здесь:

Image description

Одно значение заканчивается на 2, другое на 3; но наше приложение представляет результат деления равным 1.

Мы можем решить эту проблему несколькими способами:

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

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

Проблемы

Помимо явно неправильного поведения, существуют более тонкие проблемы с приложением в его текущем состоянии.

Неопределенное поведение для нечисел

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

Следующие входные значения прервут работу приложения:

  • 1 — пробел в конце ввода
  • 1,0 — неверный разделитель
  • 0x1 – шестнадцатеричное представление числа 1.
  • 1e0 – научное обозначение 1
  • .
  • LXIV — римская цифра для 64
  • .
  • один

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

Отсутствие обратной связи в пользовательском интерфейсе

Для ввода мы использовали <input type="number">. Этот тип ввода выполняет небольшую проверку данных — наш nominator.value будет равен пустой строке всякий раз, когда пользователь вводит что-то большее, чем число. Было бы неплохо обеспечить некоторую визуальную обратную связь, когда введенное значение неверно: например, добавив красную рамку к элементу.

Недружественный UX для ошибок

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

Image description

Image description

Image description

Есть несколько вещей, которые следует учитывать:

* NaN (не число) & Бесконечность — это то, что JavaScript выбирает в результате различных некорректных операций. Эти предположения не обязательно оптимальны для нашего варианта использования — возможно, мы хотим всегда показывать NaN или не показывать никакого результата.

* Было бы неплохо показать какое-нибудь сообщение об ошибке, объясняющее, что пошло не так.

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

Введите клавиатуру

Во время тестирования приложения я несколько раз пытался запустить вычисление, нажав Enter. Поддержка этой функции улучшит взаимодействие с пользователем (UX).

Хотите узнать больше?

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


Также опубликовано здесь


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE