Добавляйте единицы измерения в имена

GuDron

dumpz.ws
Admin
Регистрация
28 Янв 2020
Сообщения
7,758
Реакции
1,449
Credits
25,276
qedf0xhm-ur5gmy505kqus0gtzq.png
Есть одна ловушка читаемости кода, которой легко избежать, если вы о ней знаете; тем не менее она встречается постоянно: это отсутствующие единицы измерения. Рассмотрим три фрагмента кода на Python, Java и Haskell:
Код:
time.sleep(300)
Код:
Thread.sleep(300)
Код:
threadDelay 300
Сколько «спят» эти программы? Программа на Python выполняет задержку на пять минут, программа на Java — на 0,3 секунды, а программа на Haskell — на 0,3 миллисекунды.

Как это можно понять из кода? А никак. Вам просто нужно знать, что аргументом time.sleep являются секунды, а threadDelay — микросекунды. Если вы часто ищете эту информацию, то рано или поздно её запомните, но как сохранить читаемость кода для людей, никогда не встречавшихся с time.sleep?

Вариант 1: вставить единицу измерения в имя​

Вместо этого:
Код:
def frobnicate(timeout: int) -> None:
    ...

frobnicate(300)

сделаем вот так:
Код:
def frobnicate(*, timeout_seconds: int) -> None:
    # The * forces the caller to use named arguments
    # for all arguments after the *.
    ...

frobnicate(timeout_seconds=300)

В первом случае мы даже не можем сказать в месте вызова, что 300 — это таймаут, но даже если бы мы это знали, то 300 чего? Миллисекунд? Секунд? Марсианских дней? И напротив, второй пример совершенно не требует объяснений.

Использование именованных аргументов — удобная возможность для языков, которые её поддерживают, но это не всегда возможно. Даже в Python, где time.sleep определяется с одним аргументом по имени secs, мы не можем вызвать sleep(secs=300) из-за особенностей реализации. В таком случае можно присвоить имя значению.

Вместо этого:
Код:
time.sleep(300)

сделаем так:
Код:
sleep_seconds = 300
time.sleep(sleep_seconds)

Теперь в коде нет неоднозначностей, и он читаем даже без обращения к документации.

Вариант 2: использовать строгие типы​

Вместо вставки единиц измерения в имя можно использовать более строгие типы, чем integer или float. Например, мы можем использовать тип duration.

Вместо этого:
Код:
def frobnicate(timeout: int) -> None:
    ...

frobnicate(300)

Сделаем вот так:
Код:
def frobnicate(timeout: timedelta) -> None:
    ...

timeout = timedelta(seconds=300)
frobnicate(timeout)

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

Область применимости​

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

Например, возвращайте не такое:
Код:
{
   "error_code": "E429",
   "error_message": "Rate limit exceeded",
   "retry_after": 100,
}

а такое:
Код:
{
   "error_code": "E429",
   "error_message": "Rate limit exceeded",
   "retry_after_seconds": 100,
}

Не создавайте таких файлов конфигураций:
Код:
request_timeout = 10

лучше выберите один из этих вариантов:
Код:
request_timeout = 10s
Код:
request_timeout_seconds = 10

И не проектируйте бухгалтерское CLI-приложение таким образом:
Код:
show-transactions --minimum-amount 32

выберите один из этих вариантов:
Код:
show-transactions --minimum-amount-eur 32
Код:
show-transactions --minimum-amount "32 EUR"