Большинство сред визуального программирования не находят достойного применения. Как вы думаете, почему? Они нацелены на замену синтаксиса кода и бизнес-логики, но разработчики никогда эти аспекты не визуализируют. Напротив, они визуализируют переходы состояний, схемы распределения памяти или сетевые запросы.
На мой взгляд, те, кто работает с визуальным программированием, гораздо больше преуспеют, если начнут с аспектов программного обеспечения, которые разработчики уже визуализируют.
▍ Контекст
Каждые несколько месяцев кто-нибудь да выпускает очередной красивый язык визуального программирования, который выглядит, например, так:
Конкретно этот смотрится очень лаконично, притом что большинство куда менее привлекательны.
Показанный алгоритм заменяет следующий псевдокод1:
def merge_sort(a):
if (length(a) == 2):
if (a[0] < a[1])
return a
else
return [a[1], a[0]]
else:
[x1, x2] = split_in_half(a)
sorted_x1 = merge_sort(x1)
sorted_x2 = merge_sort(x2)
return priority_merge(sorted_x1, sorted_x2)
Как и в примере выше, системы, о которых я говорю, пытаются заменить сам синтаксис кода.
Но всякий раз, когда эти визуальные системы появляются, мы думаем «как прикольно!», так их и не используя. Я ещё не видел, чтобы какая-нибудь из них хоть раз упоминалась при попытке решить какую-либо задачу. Почему? Почему мы продолжаем возвращаться к визуальному программированию, если его никто не использует?
Одна из причин в том, что, по нашему мнению, оно поможет менее опытным программистам. Если бы только его код не был столь пугающим. Если бы он был визуальным. Самым популярным языком программирования является Excel Formula, который выглядит так:
=INDEX(A1:A4,SMALL(IF(Active[A1:A4]=E$1,ROW(A1:A4)-1),ROW(1:1)),2)
Я проигнорирую эту причину, потому что многие аналогичные инструменты явно предназначены для опытных разработчиков. Они предполагают, что вы будете устанавливать их через npm install
или развернёте в AWS Lambda.
Почему визуальное программирование не заходит разработчикам?
Разработчики говорят, что хотят «визуальное программирование», и ты начинаешь думать «ага, давайте заменим if
и for
». Но никто никогда не создавал схему программы для чтения for (i in 0..10) if even?(i) print(i)
. Знакомые с кодом разработчики уже ценят и понимают текстовые представления для чтения и записи бизнес-логики2.
Давайте лучше посмотрим, что разработчики именно делают, а не говорят.
Разработчики уделяют время визуализации различных аспектов кода, но редко самой его логике. Они визуализируют другие стороны ПО, которые являются важными, неявными и трудными для понимания.
Вот некоторые примеры визуализации, которые я часто встречаю в серьёзных контекстах:
- Различные способы визуализации базы кода в целом.
- Схемы подключений компьютеров в сети.
- Схемы размещения данных в памяти.
- Схемы переходов конечных автоматов.
- Swimlane-диаграммы для протоколов, работающих по принципу запрос/ответ.
Вот такое визуальное программирование интересует разработчиков. Специалистам нужна помощь именно с этими задачами, для решения которых они прибегают к визуальным представлениям.
Если вы скептично настроены в отношении подобных решений, позвольте спросить: «Вам известно, как в точности размещены данные в памяти вашей системы?» А ведь неудачное размещение данных в памяти является одной из основных проблем падения производительности. При этом очень трудно «увидеть», как именно конкретный элемент данных размещён, и сопоставить это с паттернами доступа в базе кода.
Или ещё вопрос: «Известны ли вам все внешние зависимости, которые задействует ваш код, отвечая на HTTP-запрос? Вы уверены? А вы не заметили, что Боб недавно добавил вызов в сервис ограничения скорости передачи в промежуточном ПО?» Не беспокойтесь, вы узнаете об этом при следующем сбое.
Для обоих вопросов ответом обычно будет: «Я думаю, что знаю» с затаившимся страхом, что, возможно, вы всё-таки упустили нечто, что не смогли увидеть.
К сожалению, большинство визуализаций:
- Делаются наспех.
- Составляются вручную на салфетке или с помощью Whimsical.
- Редко интегрируются в стандартный рабочий поток.
Это не говорит о том, что в индустрии нет достойных решений. Некоторые техники визуализации интегрируются в среды разработки и используются очень активно:
- Инспектор просмотра элементов DOM.
- Flame-графики в профилировщиках.
- Диаграммы SQL-таблиц.
Но всё это исключения, а не что-то распространённое. Разве не круто, когда вы можете выявить проблему производительности по flame-графику? И нам нужна эта возможность для всего.
Далее я расскажу о некоторых из этих визуализаций, чтобы вы могли рассмотреть их использование в своей текущей работе или даже интеграцию в существующие среды разработки.
▍ Визуализация базы кода
В этом прекрасном выступлении показано множество способов визуализации различных аспектов базы кода. Множество! Вот несколько интересных конкретно для меня:
- Sourcetrail: иерархия классов и браузер зависимостей.
- Treemap: статистический обзор файлов в базе кода.
- Периодическое сохранение кода.
▍ Sourcetrail
Sourcetrail – это опенсорсный инструмент для визуализации баз кода (закрыт), написанный спикером из выступления выше. Вот прекрасный обзор того, как этот инструмент помогает перемещаться по базе кода. Выглядит он так:
Sourcetrail решает множество типичных проблем дизайна при визуализации кода:
- Он показывает визуализацию рядом с кодом, при наведении на который подсвечивается соответствующая визуальная часть. И наоборот, при наведении на диаграмму, подсвечивается код. Клик по зависимости переводит вас к отвечающему за неё коду (например, когда одна функция вызывает другую, или один модуль требует другой).
- Причём этот инструмент продуманным образом скрывает информацию. В базах кода зачастую присутствует слишком много связей для одновременной визуализации. Sourcetrail же изначально показывает только то, что вы, по его мнению, ищите, и для вывода дополнительной информации просит либо кликнуть, либо навести куда-либо. Его интерфейс построен так, чтобы подтягивать потоки, которые кажутся интересными, а не получать общий обзор всей базы кода. Этим он отличается от описанного далее Treemap, который сделан как раз для получения общего представления.
Однако, как показывает демо, Sourcetrail присущ ряд типичных проблем, связанных с подобным способом визуализации:
- Нет очевидных указаний на то «когда этот инструмент может понадобиться». Выполняя профилирование, вы думаете: «Мне нужен flame-граф». А когда вам нужна эта визуализация?
- Он не присутствует в тех инструментах, которые я хочу использовать. В демо показано, как пользователь переключается между Sourcetrail и Sublime. Этот тип визуализации кода и навигации по нему должен находиться внутри редактора.
▍ Treemap
В этих видео Джонатан Блоу реализует «карту дерева» для инспектирования различных аспектов кодовой базы. Исходя из видео (я его вьюер не использовал), последняя версия выглядит так:
- Каждый квадрат здесь представляет файл кода.
- Размер квадрата отражает размер файла.
- Цвет квадрата представляет смесь метрик в каждом файле вроде глубины вложенных
if
, глубины вложенных циклов, количества глобальных считываний и так далее.
С помощью подобной визуализации можно отражать и прочие метрики (size, color)
в отношении базы кода, например, (code_size, code_quality)
, (code_size, heap_access / code_size)
и тому подобное.
Даже если вы визуализируете нечто совсем простое вроде code_size
без цвета, это может оказаться очень полезным при онбординге сотрудников для работы над массивной базой кода. Типичный монолит в крупных технологических корпорациях может выглядеть так:
packages/
first_dependency/
first_transitive_dep/
second_dependency/
second_transitive_dep/
...
src/
bingo/
papaya/
lmnop/
racoon/
wingman/
galactus/
...
Вы наверняка бегло просматривали такие. Я тоже так делал, когда устраивался в крупную компанию. Когда вы клонируете такой репозиторий и просто по нему пробегаетесь, то по факту не понимаете, что конкретно в нём находится, даже в общих чертах. В примере выше оказывается, что бо́льшая часть кода находится в сервисе racoon/
(3M строк кода) и second_transitive_dep/
(1M строк кода). Всё остальное занимает менее 300К строк и сопоставимо с ошибкой округления. Вы можете работать с такой базой кода годами, не понимая этих базовых фактов.
▍ Диаграмма сохранения кода
Работа Рича Хики «History of Closure» содержит несколько интересных визуализаций, которые помогают понять, как базы кода Clojure развивались со временем. Вот диаграмма сгорания задач, сгенерированная Hercules CLI:
- Код, написанный в течение каждого конкретного года, представлен здесь своим цветом (например, для 2006 это красный).
- Если какой-то код удаляется или заменяется на код следующего года, его участок удаляется.
- Отслеживая цвета, можно видеть остаточный объём кода по годам. Например, код 2006 года (красный) был преимущественно удалён или заменён. А вот код 2011 года (зелёный) с момента своего написания остался практически нетронутым. То же касается почти всех лет3.
▍ Компьютерные сети и топологии сервисов
Если вы когда-нибудь начнёте использовать AWS, то заметите, что документация этой платформы полна диаграмм вроде следующей:
Думаю, эта диаграмма весьма наглядна. Она показывает все задействованные «сервисы» и их связи. В этом случае, если вы знаете, что каждый из них делает, то их взаимосвязанность будет очевидной. (Если же не знаете, придётся читать о каждой).
За время своей карьеры я составлял подобную топологическую схему для каждой команды, в которой работал, и усвоил несколько уроков:
- По мере присоединения новых людей я начинал с последней созданной схемы (в среднем за шесть месяцев), что упрощало задачу. С последнего раза также происходили кое-какие изменения.
- Каждый раз при составлении диаграммы я упускал что-нибудь важное.
- Насколько понимаю, это был самый важный технический артефакт, который я когда-либо передавал новым участникам команды.
Вопрос: «Если вы используете определения сервисов gRPC, то можете ли генерировать на их основе диаграммы?»
▍ Схемы памяти
В этой теме Reddit участник задал вопрос, пытаясь понять схему распределения памяти указателей Rc<T>:
Привет! Хочу понять схему памяти при выполнении следующего фрагмента кода из стандартной библиотеки:
use std::rc::Rc; let vec_var = vec![1.0, 2.0, 3.0]; let foo = Rc::new(vec_var); let a = Rc::clone(&foo); let b = Rc::clone(&foo);
Я представил себе примерно следующую картину её распределения. Верна ли она? Спасибо!
На это один из пользователей ответил такой схемой:
Заметьте, что изначальный код не изменился. Единственное, что указано в ответе – это скорректированная диаграмма. Дело в том, что для задавшего вопрос схема является более эффективной формой представления его мысленной модели, поэтому её корректировка влияет именно на эту модель, но не на сам код.
Завершается обсуждение так (выделение моё):
Здравствуйте! Благодарю за то, что пролили свет на мой вопрос.
Вот почему важно визуальное программирование: оно зачастую отражает то, что люди представляют у себя в голове (или пытаются представить). Генерация хорошей схемы позволяет прояснить картинку в уме.
В книге «Programming Rust» очень активно используются схемы распределения памяти:
Вопрос: «Можете ли вы сгенерировать эти диаграммы прямо из аннотаций типа
struct
?»
В этом языке есть ещё один способ «распределения памяти»: его модель владения. Независимо от формы и размера данных в памяти, различные ссылки «владеют» другими ссылками, формируя таким образом дерево. Принцип владения лучше всего описывается этой схемой из «Programming Rust»:
Вопрос: «Можете ли вы генерировать деревья владения на основе исходного кода Rust?»
▍ Конечные автоматы
Эти штуки довольно стандартны. В документации Idris с их помощью демонстрируют, о чём пойдёт речь, когда предстоит раскрыть тему новых принципов моделирования конечных автоматов в системе типов. Я считаю, что этот пример полезен на двух уровнях:
- Если вы знакомы с диаграммами переходов состояний, то сможете с ходу понять происходящее.
- Вы, скорее всего, не знакомы с нотацией кода конечного автомата, поэтому будет очень кстати иметь для него альтернативный вариант представления.
Вопрос: «Можете ли вы генерировать эти диаграммы напрямую из аннотаций типов Idris?»
Но вам не нужно придерживаться строгих диаграмм конечных автоматов из универсального языка моделирования. Для чего используются эти состояния?
PaymentIntent является основным объектом, который Stripe использует для представления выполняющегося платежа. С платежом может произойти очень многое, в связи с чем в этом инструменте реализован довольно сложный конечный автомат. Мы с Мишель Бу и Изабель Бенсусан в 2019 году составили для этого механизма вот такую диаграмму конечного автомата, которая показана ниже. Тогда это была одна из первых «диаграмм» в его документации.
Она отражает различные состояния, в которых может находиться PaymentIntent
, сопровождая каждый своим UI:
Занятный пример конечных автоматов и их формализации показан в лекции Лесли Лампорта на тему коммита транзакций в TLA+.
▍ Swimlane-диаграммы для визуализации обмена запросами/ответами
Клиент-серверная архитектура на основе запросов/ответов может становиться очень сложной, и я нередко видел, как люди создают для их отслеживания swimlane-диаграммы.
Вот хороший пример из документации Stripe. В нём показаны все запросы/ответы, происходящие, когда клиент оформляет заказ, сохраняет выбранный способ оплаты и, собственно, платит:
Если вы такой ещё не видели:
- В столбцах указан тот, кто выполняет запрос (компьютер или человек).
- Каждая рамка – это действие, которое они могут выполнить.
- Каждая стрелка – это запрос/ответ между ними.
- Время, проходящее по мере выполнения запросов.
Такие схемы прекрасны. Здесь виден порядок запросов, зависимости между ними, кто и что делает и так далее. Важно то, что когда при написании кода вы видите подобный фрагмент:
const r = await stripe.confirmPayment();
то можете найти соответствующий ему запрос и просмотреть контекст, в котором он происходит, даже если в окружающем коде этого контекста нет.
Адриенна Дрейфус усердно потрудилась, чтобы создать и стандартизировать эти диаграммы в документации Stripe.
Вопрос: «Можете ли вы генерировать эти диаграммы непосредственно из комплексных тестов, которые написали для своего сервиса?»
Этот пример не отражает временну́ю составляющую при передаче сообщений. Обратите внимание, что стрелки направлены горизонтально. Но эту же диаграмму можно использовать для диагностирования состояний гонки и прочих багов, связанных с выходом из строя или проблемами тайминга.
В Aphyr зачастую используют собственную версию swimlane-диаграмм, чтобы показать, как различные процессы рассматривают состояние в распределённой системе. Например, в анализе Jepsen VoltDB 6.3 они показывают, как различные узлы базы данных могут обмениваться сообщениями:
В этой версии диаграммы для понимания проблем системы очень важно понимать продолжительность времени между запросами.
В той же статье показана интерактивная диаграмма по типу swimlane, визуализирующая результаты, полученные от инструмента Jepsen:
- Теперь каждая «дорожка» является горизонтальным, пронумерованным рядом (10, 11, 18), представляющим процесс, который считывает или записывает данные.
- В рамках отражены операции процессов, требующие время для своего завершения. Линии представляют логические связи между данными, которые процессы видят. Линии, которые нарушают линейность, отмечены недопустимыми и окрашены красным.
Ещё один крутой пример есть в документации алгоритма Double Rachet мессенджера Signal. Эти диаграммы отслеживают, что конкретно Элис и Бобу необходимо на каждом шаге работы протокола для шифрования и дешифровки очередного сообщения:
Этот протокол достаточно сложен, чтобы я мог воспринять его диаграммы как источник истины. Иными словами, если реализация алгоритма Double Rachet вдруг сделает что-либо, противоречащее диаграммам, то ошибка, скорее, окажется в коде, а не в них. Именно в перечисленных областях, на мой взгляд, визуальное программирование должно являться актуальным средством, но эта тема уже для другой статьи.
▍ Сноски
1. Я не знаю, верен ли код выше – таким я его вывел на основе диаграммы. ↩
2. Это стандартная критика систем визуального программирования, и, на мой взгляд, в этом случае толпа права. Но почему люди продолжают возвращаться к этой технологии? Что им следует делать вместо этого? ↩
3. У Рича также есть отличная презентация истории Clojure, где он рассказывает, почему эти диаграммы сгорания задач выглядят именно так: Clojure с целью сохранения стабильности кода старается избегать его переписывания, за исключением случаев исправления багов.↩
Автор: Дмитрий Брайт