Rethinking Programming Language Tutorials

After all, the reason to learn a programming language is to do something useful with it, not simply to know the language.

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

Новая лицензия для опенсорса лучше чем MIT

Юристы Blue Oak Council представили на днях новую лицензию, которая лучше MIT в простоте и юридической защите как контрибьюторов, так и пользователей.

Лицензии это, во многом, невидимая часть опенсорных проектов (навскидку: под какой лицензией идёт Node.js? Kubernetes? Lodash?) Лично я для своих репозиториев просто выбирал MIT, не желая влезать в споры про GPL, просто по умолчанию. Так вот, лицензия BlueOak лучше выражает эти самые умолчания:

Blue Oak Model License

Version 1.0.0

## Purpose

This license gives everyone as much permission to work with
this software as possible, while protecting contributors
from liability.

## Acceptance

In order to receive this license, you must agree to its
rules.  The rules of this license are both obligations
under that agreement and conditions to your license.
You must not do anything with this software that triggers
a rule that you cannot or will not follow.

## Copyright

Each contributor licenses you to do everything with this
software that would otherwise infringe that contributor's
copyright in it.

## Notices

You must ensure that everyone who gets a copy of
any part of this software from you, with or without
changes, also gets the text of this license or a link to
\<https://blueoakcouncil.org/license/1.0.0\>.

## Excuse

If anyone notifies you in writing that you have not
complied with [Notices](#)(#notices), you can keep your
license by taking all practical steps to comply within 30
days after the notice.  If you do not do so, your license
ends immediately.

## Patent

Each contributor licenses you to do everything with this
software that would otherwise infringe any patent claims
they can license or become able to license.

## Reliability

No contributor can revoke this license.

## No Liability

***As far as the law allows, this software comes as is,
without any warranty or condition, and no contributor
will be liable to anyone for any damages related to this
software or this license, under any kind of legal claim.*** 

Это вся лицензия! Что мне нравится в ней:

  • простой понятный язык, а не legalese (так тоже можно!)
  • файл LICENSE можно заменить ссылкой (у — удобно!)
  • напрямую написано, что патенты не будут мешать (чего нет в MIT и классической BSD)
  • НЕТ КАПСА (он тоже не требуется, это просто привычка юристов из времён, когда не было других способов выделить кусок текста)

В список SPDX (в частности, отсюда берётся список кратких названий для npm-пакетов) эту лицензию пока не включили, но полагаю, что это скоро произойдёт.

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

https://blueoakcouncil.org/license/1.0.0, также можно прочитать запись в их блоге. До этого Blue Oak занимались анализом и сравнением опенсорс-лицензий, результаты которого также опубликованы на их сайте.

Domain Modeling Made Functional

Короткая книга от Pragmatic Programmers, в которой описывается как совместить Domain-Driven Design и функциональное программирование.

Ключевые моменты:

  • типизировать всё — плавно превращаем разговоры с доменными экспертами в строгие типы
  • выражать моменты типа «или заказ поступил по почте, или по телефону» через sum types (enum в Rust)
  • выражать моменты «заказ это список заказанных товаров И адрес доставки И подтверждение оплаты» через product types (struct и tuple в Rust).
  • выделять ядро домена, держать его «чистым»
  • выталкивать «инфраструктуру» (всё что не бизнес-логика) наружу из ядра домена (обращения к базам данных делать или до входа в ядро, или после, но не внутри)
  • моделировать воркфлоу как композицию функций, передающих друг другу Result по необходимости

В целом, книга тянет на серию хороших докладов по содержательности, и оказалась полезной для меня как раз примерами применения абстрактной теории (список выше) к более-менее правдоподобной системе. Более того, я извлёк несколько небольших полезностей, которые можно применять и в отдельности.

Так, например, можно заставить систему типов проверять куски нашей бизнес-логики. Скажем, айди товара — это строка из шести символов (A-E и пять цифр). Наивным решением будет использовать в коде просто String в местах, где нужен айди (я буду писать примеры на Rust):

fn get_product_by_id(id: String) -> Product {
  unimplemented!()
}

Но тогда мы теряем в выразительности. Можно сделать так:

type ProductId = String;

fn get_product_by_id(id: ProductId) -> Product {
  unimplemented!()
}

Это повышает выразительность, но не добавляет уверенности, что айдишник соответствует правилам. Но если сделать так:

pub struct ProductId(String);
impl ProductId {
  fn from_string(s: String) -> Option<ProductId> {
    // check that s is correct identifier
    // return None if it is not
  }
}

…то остальной код не сможет создать значение типа ProductId не через функцию ProductId::from_string(s), которая всегда выполняет проверку строки на валидность.

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

Книга понравилась; мне кажется, что она зайдёт тем, кому любопытен DDD и/или применение функционального программирования в «жутком энтерпрайзе».

Совет: пишите свои скрипты для гита

Одна неочевидная фича гита заключается в том, что если у вас где-то в PATH лежит команда с названием git-whatever, то её можно будет вызвать как git whatever, и таким образом «расширить» гит нужной вам функциональностью.

Я в какой-то момент устал делать git push -u origin MY_NEW_BRANCH_NAME и написал кратенький скрипт, который пушит текущий бранч (push current branch):

#!/bin/sh

git rev-parse --abbrev-ref HEAD | xargs git push -u origin

Положил его в ~ (у меня он добавлен в PATH) как git-pcb, и с тех пор писать имя бранча без автодополнения приходится только при создании бранча (чего, к сожалению, избежать будет сильно дороже). То есть флоу становится такой:

$ git checkout -b MY_NEW_BRANCH
$ # do work
$ git commit
$ git pcb

При написании своих скриптов для гита часто приходится обращаться к мануалу, страницы которого доступны по тому же принципу (только в обратную сторону): если команда называется git rev-parse, то информацию про неё надо искать в man git-rev-parse.

Совет: запускайте tmux скриптом

Я использую tmux, но делаю это не очень продуктивно. Недавно я решил, что tmux — один из инструментов моей работы, поэтому нужно научиться использовать tmux эффективно.

Начал читать по этому поводу книгу tmux 2: productive mouse-free environment и в целом думать об этом, и решил заодно публиковать советы, как же лучше использовать инструменты, в том числе и tmux, под соответствующими тегами.


Сегодняшний совет: запускайте tmux скриптом, а не просто командой tmux.

Я как-то заметил, что время от времени делаю, по сути, одно и то же: запускаю tmux, после чего настраиваю окошки определенным образом: в одном окне открываю консоль, в других — разные логфайлы. Это можно описать в виде несложного скрипта, я назвал его start-tmux:

#!/bin/sh

tmux kill-session -t remote

tmux new -s remote -n shell -d
tmux new-window -t remote -n logs

tmux send-keys -t remote:0.0 "cd projects/super-secret-project" C-m
tmux send-keys -t remote:1.0 "tail -n 30 -f /var/log/super-secret-project/error.log" C-m

tmux select-window -t remote:0.0
tmux attach -t remote

В результате получается сессия с названием remote1 с двумя окнами: в одном уже открыта папка с проектом, в другом уже открыт tail -f нужного логфайла; фокус на окне с башем; можно сразу приступить к работе.

Команда send-keys отправляет в указанную панель нажатия кнопок, как если бы вы их сами нажимали, поэтому для команд, которые должен исполнить шелл, надо указать в конце C-m (это способ указать ^M, а это, в свою очередь, способ описать, что был нажат Enter. Я всё ещё пишу пост про историю консоли, да).

-t обозначает окно или панель, формат простой: sessionName:windowIndex.paneIndex. Поэтому скрипт может работать сразу с несколькими сессиями (или принимать имя сессии как параметр, например). Можно сделать разные скрипты для разных проектов, или один большой скрипт, который сразу открывает всё необходимое для нескольких проектов.

Ещё две детали про этот скрипт: в самом начале я прибиваю предыдущую сессию на всякий случай, и в конце делаю attach — первое скорее всего необязательно в большинстве случаев, а если не делать attach, то можно делать скрипты, которые меняют текущую сессию tmux — например, условный debug-mode, который создаёт новое окно, запускает в нём дебаггер и открывает рядом логи — но я пока этого не делал.

В итоге я подключаюсь по ssh и выполняю ~/start-tmux, чтобы получить готовое окружение.

1

Такое название помогает мне отделить локальный tmux от tmux на удаленной машине (и поэтому сессия на удаленной машине называется remote). Tmux в tmux работает неплохо, если забиндить C-a C-a на send-prefix; но доходить до tmux в tmux в tmux мне приходилось только несколько раз, не рекомендую.

Как ускорить код-ревью

Представим, что перед вами стоит задача — ускорить процессы код-ревью в компании: сейчас код-ревью занимает сутки, а в плохих случаях и вовсе длится неделями, из-за чего фичи в прод едут медленнее, чем хотелось бы.

Что можно с этим сделать? Одним подходом может быть «установить чёткие дедлайны для ревьюеров и бить их палками, если что-то идёт не так». Это не системный подход, поэтому давайте его сразу пропустим и перейдём к более интересному.

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

Что такое код-ревью; какие у него цели?

Код-ревью это процесс, который превращает Непроверенный Код в Одобренный Код. Это нужно, чтобы сократить количество ошибок, которые изначально не заметил разработчик, изменивший код.

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

Разработчик Олег пушит свои изменения напрямую в мастер, откуда эти изменения выезжают в продакшен. Если Олег сделал всё правильно и ничего не забыл, то нам повезло и мы ускорили пайплайн в этом месте. Но при этом есть разработчица Александра, которой придётся пересматривать свои ещё не смёрженные изменения с учётом только что вмёрженных изменений Олега. Если вам кажется, что это просто — ну, да, запустить rebase origin/master действительно просто, но что если изменения Олега меняют предположения о системе, из которых исходила Александра? В таком случае автоматический ребейз может либо выдать мёржконфликт (и это будет хорошо! Это может заострить внимание на нарушенных предположениях), либо, что страшнее, молча сделать вид, что всё нормально, хотя изменения Олега и Александры работают по отдельности и не работают вместе.

Здесь важно, что Александра и Олег меняют одну и ту же систему — если их изменения затрагивают независимые системы, или же системы, которые как-то зависят друг от друга, но отгорожены публичными интерфейсами, то всё становится сильно проще.

Ключевой момент здесь: изменения меняют предположения о системе; под словом «предположения» я имею в виду изменения, которые обладают потенциалом «каскада», где потребуется менять какую-то ещё часть кода системы. Такие изменения можно грубо охарактеризовать, разбив их на три уровня:

  • уровень интерфейсов между горизонтальными (равноправными) частями системы. Например, «чтобы показать попап, надо нарисовать в своём компоненте <Popup>{popupContent}</Popup>».
  • уровень фич. Например, «мы хотим показывать попап в такой ситуации».
  • уровень платформы. Например, «наше приложение запускается в контейнере с Node.js такой-то версии».

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

Когда мы говорим о скорости системы (в данном случае нас интересует конкретный процесс — превращение фичи из идеи в работающий в продакшене код), следует помнить о том, что latency и throughput —это две разные метрики. Например, если мы посадим половину разработчиков дежурить и проводить код-ревью как только появляется пулреквест от второй половины, то мы снизим latency (это хорошо), но также снизим и throughput (это, как правило, плохо).

В этом контексте часто говорят о time to market определенной фичи, но это не очень подходящая метрика: если мы релизим одну фичу в неделю, но зато её провели через конвейер за полдня, то у нас будет отличный ttm (зарелизили за полдня!), но отвратительный throughput (в неделю только одну фичу!).

Код

На самом деле мы хотим уменьшить latency и увеличить throughput одновременно. Это возможно за счёт снижения времени на код-ревью без снижения качества.

Важный момент: это сработает, если бутылочным горлышком системы действительно является код-ревью. Если горлышко в другом месте, то усилия по улучшению процесса ревью будут бесполезны в контексте улучшения latency/throughput (но могут, например, повысить уверенность разработчиков, что тоже бывает полезным).

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

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

В код-ревью в целом можно выделить четыре стадии:

  1. Время от создания пулреквеста до начала ревью
  2. Время ревью, где ревьюер должен осознать суть изменений, на какие предположения опирается пулреквест, не нарушены ли они. (В плохих случаях здесь также идёт анализ кода на соответствие код-стайлу, в большинстве случаев это можно вынести на шаг 1 через автоматизацию)
  3. Время правок, где автор пулреквеста исправляет замечания с шага 2; после чего мы снова переходим на шаг 1, если только на шаге 2 не было никаких замечаний.
  4. Мёрж изменений, тестирование, релиз, потенциально — отлавливание проблем в продакшене.

Даже если мы не в состоянии изменить структуру, мы можем повлиять на количество итераций в цикле 1-3. Один из главных рисков — во время цикла система изменяется таким образом, что придётся ещё раз вносить изменения относительно предположений, или же отлавливать баги после; и то, и другое — переработка, необходимость которой возникла из-за долгих процессов ревью.

Чтобы снизить количество такой переработки надо снижать количество потенциально изменчивых предположений внутри пулреквеста. Как самый простой способ: делать пулреквесты небольшими, потенциально даже меньше, чем фича (таким образом, для реализации и деплоя фичи в мастер должны попасть несколько пулреквестов). Это снижает вероятность переработки тем образом, что когда Олег приходит менять, скажем, Popup, он учитывает потребителей, которые уже в мастере, и может автоматически отрефакторить все вызовы попапа на новый синтаксис.

Другой способ снизить количество изменчивых предположений — сделать куски системы более независимыми, ограничить их взаимодействие более стабильными интерфейсами и протоколами; тогда нам может помешать только два момента: те изменения, которые ломают внешний API, и те ситуации, где мы зачем-то зависим от видимых проявлений, не являющихся частью формального внешнего API (закон Хайрума гласит, что, при достаточно большом количестве потребителей, на каждое наблюдаемое поведение системы найдётся потребитель, который зависит от этого поведения).

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

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

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

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

Люди

До этого текст делал неявное предположение, что мы работаем с идеальными ревьюерами, которые ответственно относятся к проведению ревью и чьи комментарии действительно стоит учитывать, а не игнорируют до последнего и придираются к запятым вместо вдумчивого анализа изменений. К сожалению, в реальном мире так далеко не всегда. С учётом того, что работать нужно с теми людьми, которые есть, надо упрощать проведение хорошего, внимательного ревью, и не поощрять поверхностное.

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

Часто говорят о роли код-ревью в ещё трёх процессах: снижение code ownership, распространение знаний о коде, обучение best practices и программированию в целом. Это всё полезные цели, но если мы достигли точки, где код-ревью само по себе стало заметной проблемой, то стоит вынести, хотя бы временно, их из критического пути в асинхронные методы коммуникации (например, рассылки и чейнджлоги).

Проблема code ownership заключается в том, что у человека возникает ощущение, что какой-то кусок кода принадлежит ему и враждебно относится к попыткам изменить этот кусок. Это решается предотвращением: если обозначить владельцами команды, то кусок кода не мой, но наш, моей команды. Это работает, если над системой работают несколько независимых команд — тогда включается механизм мы-и-они.

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

У людей помимо всего прочего есть свои biases, но у каждого человека их набор и их проявления более-менее стабильны, поэтому со временем можно составлять «портрет» ревьюера. Например, Игнат всегда находит, что откомментировать, в любом пулреквесте, поэтому назначая ревью на него, стоит предположить, что случится пинг-понг.

Большая картина

Теперь немного посмотрим на картину в целом: как определить, что процесс код-ревью выстроен хорошо?

  1. Ошибки в продакшене — редкость, и обычно связаны с непредсказуемыми факторами: код-ревью выполняет свою главную роль повышения стабильности.
  2. Код-ревью редко превращается в пинг-понг: ревьюеры правильно и полноценно коммуницируют замечания.
  3. Время прохождения код-ревью обладает небольшой вариативностью: не бывает гигантских или чрезвычайно сложных пулреквестов.
  4. В системе нет мест, которые страшно менять: у разработчиков есть вера в то, что код-ревью поймает возможные ошибки.
  5. Через ревью проходят как мелкие фичи, так и большие инфраструктурные изменения.

Каждый из пунктов при желании можно измерять и отслеживать — но стоит сравнивать проект только с этим же проектом, для каждой системы целевые значения метрик свои: требования к надёжности в NASA и стартапе разные.

Слишком длинно, давай вкратце

  1. Пулреквесты должны быть маленькими: меньше строк кода — меньше предположений — быстрее ревью,
  2. Пулреквесты могут быть на половинку фичи, используйте фича-флаги: ранняя фиксация предположений в мастере снижает вероятность их нарушения,
  3. Чем меньше ревью требует ручного анализа, тем лучше: разделяйте систему на независимые и отграниченные интерфейсами части, чтобы снизить количество необходимых предположений.