Следующая статья взята из руководства Kiwi.com по внутреннему проектированию. Мы подумали, что это может пригодиться и другим, поэтому решили поделиться им со всем миром здесь.

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

Нечего забрать

Совершенство достигается не тогда, когда нечего добавить, а когда нечего убирать

- Антуан де Сент-Экзюпери

Давайте рассмотрим пример kw.booking.additional_booking_management (внутренний модуль для управления надстройками для бронирования авиабилетов). Этот код больше нельзя найти в master (очевидно, мы не собираемся просто оставлять его валяться в качестве плохого примера), но это просто означает, что мы видели, как соблюдение этих правил работает на практике.

Это название модуля точно описывает, для чего он нужен: для управления дополнительными бронированиями. Но можно ли что-нибудь здесь забрать? Тщательно обдумывая это, мы понимаем, что суффикс _management на самом деле не добавляет ценности. Это просто общее слово, которое мы можем добавить к чему угодно, но оно не несет никакой полезной информации. Если мы просто назовем это additional_bookings, что вы можете предположить, что он делает с AB, кроме управления ими? Вы мало что можете сделать.

Почему мы тратим столько времени на размышления?

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

Если вы не понимаете, как это важно, просто сравните эти две строки:

total_building_area_in_square_meters = number_of_buildings *(width_of_buildings_in_cm / 100) * (length_of_buildings_in_cm / 100)
total_area_m2 = len(buildings) * (width_cm / 100) * (height_cm / 100)

Вы не потеряете никакой значимой информации, но во втором примере она занимает примерно половину меньше места. Использование вдвое меньшего объема текста означает более быстрое сканирование кода для человека-читателя - если вы прищурите глаза и сами буквы будут неразборчивыми, вы все равно сможете определить, какая это часть кода, просто увидев что-то вроде •••••• = len(••••••) * (•••••• / 100) * (•••••• / 100) . Уже полезно просто увидеть, что в этой строке выполняется какой-то расчет, а не что-то вроде if •••••• > ••••••: или def ••••••(••••••, ••••••):. В более длинной версии вы просто не сможете увидеть всю строку кода одним взглядом. Это все еще звучит так, как будто это не имеет большого значения, но теперь вы должны рассмотреть контекст, в котором вы чаще всего читаете код. Вот некоторые примеры, когда вы пытаетесь:

  • Отследите, как переменная может получить конкретное значение в производственной ошибке: пройти через 10 уровней в стеке под большим давлением паники PagerDuty и Slack.
  • Поймите, как именно вы должны называть этот модуль в коде, который вы пишете, без необходимости понимать его внутреннюю работу.
  • Найдите пример того, как этот модуль использует ведение журнала, базы данных или что-то еще, чтобы вы могли скопировать и вставить что-то в свой собственный код.
  • Определите, где именно вы должны вызвать новую функцию, которую вы собираетесь написать для какой-то совершенно новой функции.
  • Убедитесь, что этот код не нарушает окружающих изменений, которые вы просматриваете.
  • Вносите одинаковые изменения во всех местах репо (рефакторинг использования БД, параллелизм или исправление ошибок статического анализа)

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

Но также может случиться так, что кода окажется слишком много, чтобы просто бездумно сканировать. Длинные строки, обходные способы манипулирования значениями и чрезмерное использование переменных могут привести к тому, что ваша задача «найди то, что мне нужно здесь» будет зависеть от нового «понять этот код» задача. Если раньше вы могли полагаться на свои инстинкты и опыт, чтобы понять ровно столько кода, чтобы добиться прогресса, теперь вам нужно переключиться, чтобы сознательно попытаться понять, что происходит, поскольку ваша рабочая память сейчас хранит новые предметы. Если код слишком сложен, одним из таких дополнительных элементов может быть buildings_count is just len(buildings) '. Это не имеет отношения к вашей задаче, если вы просто пытаетесь увидеть, как счетчик зданий передается или привязывается к системе регистрации, но при этом тратит ценное пространство в вашей краткосрочной памяти.

Итак, теперь мы понимаем, что последствия могут быть более серьезными, чем просто «заставить людей читать на одну секунду дольше». Вы выбили инженера из их потока и прервали его первоначальную задачу, которую он, возможно, не сможет возобновить, не потратив на это несколько минут. получение всей их контекстной информации обратно. Теперь нам нужно осознать еще одно: это не только серьезнее, чем кажется, но и гораздо чаще. Этот не требует от меня особых объяснений, просто сделайте следующее, не задумываясь: придумайте, сколько раз вы ожидаете, что люди прочитают последний фрагмент кода, который вы написали в следующем году. Теперь подумайте еще немного и сделайте точные оценки:

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

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

Примечание: не оптимизируйте только для возможности сканирования

Хотя возможность сканирования очень важна, это не единственный аспект удобочитаемости. Сравните это с приведенным выше:

a_m2 = len(bdgs) * (w / 100) * (h / 100)

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

  1. Читаемость = Сканируемость + Понятность
  2. Люди естественным образом склонны отдавать предпочтение понятности, хотя Сканируемость не менее важна, поэтому вам необходимо активно бороться с этой предвзятостью.

Заставлять людей узнавать что-то новое - это нормально

Итак, теперь мы вернулись к kw.booking.additional_bookings. Что-то здесь все еще не так. Это имя, дополнительные бронирования ... Оно довольно длинное, это не может быть самым простым способом описать запросы на обслуживание пассажиров и другие дополнения ... приложения ... дополнительные услуги ... дополнительные услуги. Дополнительно. Хм. Опять же, это не то, что имеет идеально подходящее словарное определение, но если вы представите себе, что видите бронирование, и видите, что оно описано как «3 дополнения», разве вы не сможете интуитивно понять общую идею, стоящую за ним? И неужели вы просто не вспомните это в следующую тысячу раз, когда увидите эту фразу?

  • У нас есть простой способ распространять информацию среди людей - название говорит само за себя, если вы немного задумаетесь над ним.
  • Фраза или концепция достаточно популярны для того, чтобы естественный отбор начал действовать - когда со временем они будут использоваться достаточно часто, лучшее название завоюет популярность и заглушит старое название (имена).
  • Это изменение легко сохранить согласованным, не нужно беспокоиться о появлении незначительных изменений. Это было бы проблемой, например, если бы мы попытались сделать что-то вроде добавления столбцов стандартных меток времени создания во все таблицы в базе данных. Если мы решим, что created_at - лучшее имя для этого, но не будем активно его применять, то только вопрос времени, когда кто-то вместо этого назовет свой столбец created. Или inserted_at. Или creation. Или creation_date. В случае created_at, поскольку так легко непреднамеренно создать варианты, нам пришлось создать репозиторий для декларативных моделей баз данных и зафиксировать имя в многократно используемом фрагменте кода.

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

Подумайте в первую очередь о пользователе

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

Если вы заботитесь о своем коде и поддерживаете его в чистоте, чтобы ваша работа была легкой, вы можете получить красивый, чистый дизайн; например, размещение ваших функций обработки данных, таких как get_additional_detail в models.py. Это упрощает людям работу над kw.booking.extras, отлично! Но легко ли тогда работать с kw.booking.extras? К сожалению, это не так. Если кто-то просто хочет использовать ваш extras модуль, теперь им нужно отправиться на охоту, чтобы выяснить, как вы спроектировали его внутреннее устройство и как на самом деле можно использовать код. Оказывается, вам придется сделать from kw.booking.extras.models import get_additional_detail, что просто непредсказуемо; никто не догадался бы об этом, не увидев кода.

Как бы мы это решили? Просто действуйте так, как если бы вы были пользователем, и подумайте о том, как вы собираетесь использовать модуль, прежде чем приступить к работе над ним. На самом деле есть очень простой способ сделать это: написать тесты. Тесты в основном заключаются только в том, что вы используете свой собственный код.

Мы пришли к следующим выводам:

  1. Доступ к kw.booking.extras.models просто излишне неудобен. Простой импорт в вашем __init__.py может позволить вашим пользователям писать импорт более простым и очевидным способом: from kw.booking.extras import get_additional_detail
  2. Если бы автор модуля подумал о пользователях и представил все, что вам нужно в __init__.py, мы могли бы изменить наш импорт, чтобы сделать вызовы функций более понятными с помощью пространства имен: from kw.booking import extras и использовать его как extras.get_additional_detail(123, 456).
  3. get_additional_detail - глупое имя для нашей функции, которая возвращает дополнительную информацию из базы данных по идентификатору в виде словаря. Было бы больше смысла как get_extra_details, так что теперь у нас есть from kw.booking.extras import get_extra_details.
  4. Но если предполагаемое использование extras.get_extra_details(123, 456), почему мы упоминаем слово extra дважды? Это имеет такой же смысл, как extras.get_details(123, 456), или, если вы хотите быть экстремальным, даже extras.get(123, 456).

И это была история того, как мы ушли от

from kw.booking.additional_booking_management.models import (
    get_additional_detail, add_additional_flights, expire_additional_bookings
)
additional_booking = get_additional_detail(bid, abid)
add_additional_flights(additional_booking)
expire_additional_bookings(bid)

to

from kw.booking import extras
extra = extras.get(bid, eid)
extras.add_flights(extra)
extras.mark_expiries(bid)

Сигнатуры функций не менее важны

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

Когда люди пытаются использовать написанную вами функцию или метод, в идеале им не нужно вдаваться в подробности, кроме проверки списка параметров. Рассмотрим, например, requests.get. Вы увидите такие параметры, как url, headers, timeout, auth. Мы можем извлечь некоторые уроки из библиотеки Кеннета Рейца.

  • Сведите количество требуемых параметров к минимуму (обычно это один или два на функцию).
  • Установите разумные значения по умолчанию для дополнительных параметров, которые упростят работу с функцией в интерактивном интерпретаторе. Это не цель, но это самый простой способ подумать о том, каким должно быть значение по умолчанию - какими бы вы хотели, чтобы значения были, если бы вы просто тестировали функцию прямо сейчас?
  • Предпочитайте объединять параметры, когда это имеет смысл: вместо того, чтобы запрашивать bid, booked_at, partner, market, запрашивайте только booking и обращайтесь к booking.id, booking.timestamp, booking.partner, booking.market
  • Упорядочивайте необязательные аргументы ключевого слова по их ожидаемой частоте использования.
  • Явно запретите запутывающее использование вызовов функций. Если у вас есть такая функция, как book_flight(id, sandbox=True), люди неизбежно начнут использовать ее, как book_flight(123, False), где никто не сможет понять, что означает второй параметр, просто взглянув на него. Вместо этого вы можете определить свою функцию следующим образом: book_flight(id, *, sandbox=True), и люди будут вынуждены выписывать sandbox=False каждый раз, когда они используют вашу функцию. Однако эта функция доступна только в Python 3.

Прямой объект

Подумайте, что является «прямым объектом» вашей функции, если мы можем использовать здесь на минуту грамматическую аналогию. Допустим, у вас есть такое предложение: «Я пошел купить мед в Tesco в 22:00». Здесь глагол to buy требует прямого объекта, которым будет мед. Глагол не имеет смысла без цели - нельзя просто сказать «Я пошел покупать», поэтому нам всегда нужно указывать это. Мы также указываем еще две вещи: источник меда и время покупки, но это необязательные детали, без которых предложение не утратило бы смысл.

Итак, как это связано с программированием? Грамматическая структура здесь на самом деле очень хорошо соответствует функциональным сигнатурам. Если бы нам пришлось записать это предложение как вызов функции, это было бы что-то вроде buy('honey', source='Tesco', time=arrow.get('22:00')). Обратите внимание, как отображение действия buy на код по-прежнему оставляет его с такими же семантическими ограничениями. Вызов buy по-прежнему не имеет смысла, как этот, без цели: buy() - точно так же, как вызов requests.get() не имеет смысла без URL-адреса.

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

В итоге

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