Pull to refresh

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

Reading time3 min
Views19K
Original author: Ruud van Asseldonk

Есть одна ловушка читаемости кода, которой легко избежать, если вы о ней знаете; тем не менее она встречается постоянно: это отсутствующие единицы измерения. Рассмотрим три фрагмента кода на 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"
Tags:
Hubs:
Total votes 76: ↑68 and ↓8+60
Comments114

Articles

Information

Website
moskva.beeline.ru
Registered
Founded
Employees
1,001–5,000 employees
Location
Россия
Representative
Bee_brightside