fuck UB

Viktor Love
4 min readJul 13, 2019

--

Я хочу рассказать о довольно важной и банальной идее: код должен адекватно работать на всей области определения.

“Адекватно” — не значит идеально. Код должен просто выдавать осмысленнный результат. Даже если результат “FuckYouException”. Никогда не должно быть неожиданностей.

Пример

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

Например, за 1 штуку товара полагается 0% скидки, а за 2 — уже 2% скидки. Вот она:

getDiscount

Посмотрим налево

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

Реализация от Димы
Реализация от Васи

Два кодера реализовали абсолютно разные штуки, потому что отрицательное количество — бессмыслица и на эту штуку нет спецификации. Undefined behavior в чистом виде.

Посмотрим направо

Но вот еще: этот код писали для маленького розничного интернет-магазина, а не для оптовика. А теперь представим, что магазину очень повезло и кто-то решил купить много товара. Давайте посмотрим в счастливую бесконечность:

Скидка 101% для наших лучших покупателей

А теперь мы еще денег должны. Вообще отлично.

А правильно так

Если функция ожидает, что параметр count будет от 0 до 100, то она должна это проверять и хуячить исключениями.

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

Для данного конкретного примера: область count < 0 стоит отсчеь, а область больших count можно обрабатывать так:

if (count > 90) 
return 0.99

Неадекватные случаи?

Конечно, может показаться не очень разумным делать специальную обработку случая count < 0.

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

Часть целого

А вот что важно помнить о функции getDiscount — всем на неё насрать. Таких функций в проекте огромное количество. А красивые графики строятся только для функций из вымышленных примеров, которыми мы пытаемся донести очередную идею.

Функция getDiscount используется внутри такого кода:

let itemPrice = count * (baseItemPrice * (1 - getDiscount(count))

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

А функция getItemPrice используется внутри друго кода:

let totalPrice
= order.items.map(getItemPrice).reduce(+)
+ getDeliveryPrice(order.postalAddress)

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

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

А вообще, это плохой пример

Функция getDiscount довольно примитивна и тупа. Она принимает одно число и возвращает другое число. Реальный код очень редко похож на это. Он скорее похож на “принимает/читает кучу херни и возвращает аггрегированную херню”.

Бла-бла

Почему бы просто не написть тест?

TDD как парадигма “сначала подумай о кейсах, потом пиши код” может помочь, но только если правильно думать о кейсах.

А вот просто тесты не могут покрыть все многообразие взаимодействия кода. Что-то останется обязательно пропущенным и аукнется вам.

Локальный оптимум

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

Увы, этот подход не устойчив при эволюции кода. Да, у вас нет времени сделать хорошо сейчас. Но разве вы уверенны, что потом у вас (или другого разработчика) будет гораздо больше времени на то, чтобы обнаружить проблему, разобраться как правильно её пофиксить и написать корректный код?

Заезженная картинка о том, сколько стоит фиксить баг

HighLoad

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

В бизнес-коде корректная проверка может потребовать загружать дополнительные данные. А это еще сильнее сказывается на скорости.

Увы, я не знаю общего решения, кроме как “писать бенчмарки и думать”. Хотя, вы всегда можете завернуть safety checks в #debug, чтобы хоть как-то улучшить ситуацию.

Fail fast?

Это не совсем fail fast, но это — его логичное продолжение. Типичный рассказ о fail fast включает в себя “падай за пределами области определения”, но не включает в себя ”думай шире”.

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

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

--

--

Viktor Love
Viktor Love

Written by Viktor Love

Software Engineer from Ukraine. TypeScript, React, C#, Angular.

No responses yet