Создание приложения-разделителя: сколько дел можно разделить на одну операцию?
9 марта 2023 г.Существует большая разница между приложением, которое нормально работает только в идеальных условиях, и приложением, которое ведет себя ожидаемым образом при любых обстоятельствах. Для пользователя первое создает впечатление, что продукт почти готов — с некоторыми ошибками тут и там. На самом деле, сделать приложение надежным — большая работа для программиста. Необходимо выявить все случаи, требующие особого внимания: ошибки, а также ситуации, в которых ожидаемый результат не очевиден.
Обдумывать все, что может пойти не так при выполнении кода, и готовиться к ним — важная часть работы программиста. В то же время эта тема не обязательно достаточно освещена в учебных материалах для начинающих. Давайте посмотрим на этот процесс в действии на простом примере.
Истории пользователей
Истории пользователей объясняют, чего пользователь хочет достичь с помощью приложения, которое мы создаем. Здесь все просто — разделите одно число на другое, а затем покажите результат на экране.
Интерфейс
Можно предположить, что пользователи или наше приложение будут знакомы с этой нотацией:
Давайте используем это как основу для интерфейса приложения: числитель и знаменатель становятся числовыми входными данными, а результат отображается рядом с ними.
Реализация
Я создал простую реализацию приложения. Он доступен здесь и выглядит следующим образом:
Тело 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
Тот же результат будет отображаться для пользователей нашего приложения:
Есть несколько подходов, которые можно использовать для устранения такого поведения. Одним из очевидных решений было бы повысить точность наших расчетов и округлить результат. Когда мы имеем дело с деньгами, нам нужно знать результат с точностью до 0,01
— любая разница ниже не имеет смысла.
Слепое округление вводит новый пограничный случай: если числитель достаточно мал, в какой-то момент результат будет округлен до 0.
Мы могли бы продвинуть проблему дальше, но с умным округлением: попытаться оставить в результате несколько значащих цифр. Это усложнило бы решение, но в зависимости от сценариев использования, которые мы хотим поддерживать, это может иметь смысл.
Максимальное безопасное целое число
Числа с плавающей запятой позволяют хранить более широкий диапазон значений по сравнению с целыми числами. Недостатком является неточное представление, о котором мы говорили выше. По мере дальнейшего отклонения числовых значений от 0 отпечаток также становится больше.
В какой-то момент JavaScript не может обеспечить для нас надежность целочисленных значений — достаточно велико впечатление, что два разных целых числа представлены одинаково.
JavaScript сохраняет значение края как:
>> Number.MAX_SAFE_INTEGER
9007199254740991
С этого момента наши расчеты становятся ненадежными, как вы можете видеть здесь:
Одно значение заканчивается на 2, другое на 3; но наше приложение представляет результат деления равным 1.
Мы можем решить эту проблему несколькими способами:
* ограничить максимальную ценность, которую пользователь может внести в приложение * измените представление данных на BigInt, тем самым поддерживая большие целые значения, но ценой отказа от поддержки дробей
Как видите, опять же, здесь нет идеального решения: в зависимости от поддерживаемых вариантов использования тот или иной подход может быть лучше.
Проблемы
Помимо явно неправильного поведения, существуют более тонкие проблемы с приложением в его текущем состоянии.
Неопределенное поведение для нечисел
В нашем приложении мы подключили пользовательский ввод непосредственно к методу parseFloat
, не пытаясь обрабатывать ошибки.
Следующие входные значения прервут работу приложения:
1
— пробел в конце ввода1,0
— неверный разделитель0x1
– шестнадцатеричное представление числа 1.1e0
– научное обозначение 1 .
LXIV
— римская цифра для 64 .
один
Для каждого из этих значений вы можете оценить стоимость и выгоду от поддержки такого рода входных данных. Если все сделано правильно, вы можете получить лучший пользовательский опыт в обмен на небольшие усилия во время программирования. В то же время — чем больше поддерживаемых форматов, тем больше пограничных случаев, а неправильное разрешение может привести к непредвиденному поведению приложения.
Отсутствие обратной связи в пользовательском интерфейсе
Для ввода мы использовали <input type="number">
. Этот тип ввода выполняет небольшую проверку данных — наш nominator.value
будет равен пустой строке всякий раз, когда пользователь вводит что-то большее, чем число. Было бы неплохо обеспечить некоторую визуальную обратную связь, когда введенное значение неверно: например, добавив красную рамку к элементу.
Недружественный UX для ошибок
В случае ошибок наше приложение пытается вычислить результат и показывает ошибку одним из многих способов:
Есть несколько вещей, которые следует учитывать:
* NaN
(не число) & Бесконечность
— это то, что JavaScript выбирает в результате различных некорректных операций. Эти предположения не обязательно оптимальны для нашего варианта использования — возможно, мы хотим всегда показывать NaN
или не показывать никакого результата.
* Было бы неплохо показать какое-нибудь сообщение об ошибке, объясняющее, что пошло не так.
Опять же, лучший подход зависит от контекста, в котором будет использоваться приложение.
Введите клавиатуру
Во время тестирования приложения я несколько раз пытался запустить вычисление, нажав Enter. Поддержка этой функции улучшит взаимодействие с пользователем (UX).
Хотите узнать больше?
В этой статье мы увидели, сколько проблем может возникнуть даже в самом простом приложении. Как только мы узнаем, как приложение должно вести себя во всех обнаруженных нами пограничных случаях, следующим шагом будет реализация этого поведения и обеспечение того, чтобы оно оставалось таким. В дальнейшем мы увидим различные автоматизированные средства контроля качества.
Также опубликовано здесь
Оригинал