Николай Ланец
29 сент. 2021 г., 11:20

Как low-code проект может помочь в обучении программированию

Всем привет!

Сегодня хочу поделиться небольшим открытием (новая версия docker и плагины для него) и как я к нему пришел через свой новый pet-project. Вообще сложно назвать его проектом (потому что я постоянно что-то экспериментирую и часто это забрасывается). Но тем не менее, может что-то и вырастит.

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

Мой новый проект - это эксперименты в области low-code. Вот пара заметок на хабре: раз, два и три. И хотя это направление вызывает у программистов больше негатива, чем позитива, я все же считаю, что это действительно перспективное направление. То есть сейчас это конечно же далеко от желаемого уровня и сложно применимо в реалиях, тем не менее сама идея имеет место быть. Все-таки при должном уровне проработки действительно часть бизнес-процессов можно перенести в эту область. Вот один из таких проектов с открытым кодом: https://n8n.io и видео оттуда:


Так может не все сразу понятно, но если коротко: специалист в интерфейсе набрасывает логику, накидывая специальные компоненты, конфигурируя их и устанавливая связи. На выходе что-то там происходит. То есть здесь не написали ни строчки кода, но какая-никакая, но логика выполняется. Это и есть low-code (буквально Мало кода. Еще есть термин zero-code (ноль кода), но судя по всему это одно и тоже).

Так чем же мне все это интересно и как это связано с программированием (если тут программировать и не надо)? На самом деле правильно говорят противники этой идеи: чтобы всем этим пользоваться, все равно надо уметь программировать, потому что не программисту реально многие вещи не понятны. Они не понимают типов данных, не всегда могут выстроить логические цепочки, не могут создать набор правильных переиспользуемых компонентов и т.д. и т.п. И я в этом с ними согласен. Но я считаю, что это как раз может быть неплохим материалом именно для изучения программирования, то есть формирования у себя этого правильного понимания и восприятия. То есть когда мы пытаемся что-то с этим сделать, мы испытываем недостаток базовых знаний. Соответственно, начинаем у себя эти знания прокачивать. И потом начинаем понимать, что такие-то знания полезны для тех или иных ситуаций. И сейчас я это покажу на своем личном примере.


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

Итак, логика предполагается такая:

1. За основу будет взята платформа @prisma-cms/nextjs-nexus

2. Писаться будет все соответственно на TypeScript/GraphQL/React и т.п. (то есть не планируется пока установка какого-либо дополнительно программного обеспечения, все штатными средствами).
Отдельно уточню, что выполняться будет на линуксе, так что если у вас другая операционная система, то что-то может.

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

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

5. Если нигде не возникло ошибки, то успех.

Вот примерная логика:


И вот здесь сразу можно усмотреть несколько логических вопросов. Самый главный вопрос: как организовать логику так, чтобы конечный успех был только в случае успеха для всех контейнеров. То есть здесь нужен такой обработчик, который проверит, чтобы истинных результатов было столько же, сколько и проверок. Уточню в чем здесь такая закавырка: в коде мы просто обычно на месте сразу прописываем обработчики и тут же решаем что делать дальше. А в подобных системах логика чуть другая: здесь есть точка входа и точки выхода. То есть в текущей точке логики я должен отправить дальше полученную информационную единицу и там уже должно быть принято решение что и как. Чтобы было понятней, отследите логическую целочку для одного отдельного контейнера. Получается так: проверяем в списке контейнеров конкретный контейнер, если он запущен, то идем дальше на успех; если не запущен, то идем на ноду запуска контейнера; если получилось запустить, то дальше на конечный успех, а если не получилось, то на ошибку. И здесь вроде все понятно и логично. Но у нас произвольный список контейнеров. Если их будет два и больше, то какой-то контейнер проверка выполнится быстрее и придет на конечный Успех. И что делать в таком случае? Где гарантия, что другой контейнер тоже успешно проверится? Здесь сразу уместно упомянуть, что есть процессы последовательные, а есть параллельные (в JS на собеседованиях любят спрашивать про синхронный/асинхронный код).

В итоге получается, что даже еще до этапа написания конечного кода, уже можно сломать мозг о рисование логической схемы. Хотя казалось бы "Чего нам стоит дом построить? Нарисуем, будем жить". К слову, не раз видел, как люди пишут код, не проработав как следует логику, будучи уверены, что когда код будет написан, то и логика сама сабой разрешится. Этакий самообман программиста. Уверяю вас, так не бывает. Компьютер поймет вас ровно на столько, на сколько вы с ним изъяснитесь.

Ну да ладно, сейчас мы не будем сильно углубляться в логические детали, а рассмотрим пару технических моментов, с которыми я столкнулся, и попробуем разобраться в чем же здесь основы программирования и как это нам может помочь в освоении этого самого программирования.
Как я и сказал, здесь мы будем пользоваться базовыми средствами, практически не прибегая к сторонним готовым средствам. Первая задача - получить список запущенных контейнеров в докер-проекте. Запуск такого проекта расписывался в этой статье. Чтобы вывести список запущенных служб, надо в терминале выполнить команду docker-compose ps
Вот результат выполнения:


Теперь я хочу выполнить это средствами node-js. Для этого можно воспользоваться командами типа exec или spawn. Я понимаю, что рассмотрение этих команд детально - совсем не материал для начального уровня, но все же упомянуть их я должен. И тут же оставлю ссылочку на понравившуюся мне статью про эти команды (правда, она на англ): https://www.freecodecamp.org/news/node-js-child-processes-everything-you-need-to-know-e69498fe970a/

Не вдаваясь в подробности, объясню сам принцип: мы ноде говорим "выполни команду docker-compose ps" и верни нам результат, дальше мы сами решим что с этим результатом делать.

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


Скачать книгу можно на официальном сайте автора: http://www.stolyarov.info/books/programming_intro/vol1
Ссылка на книгу (ее можно не заметить там): http://www.stolyarov.info/books/pdf/progintro_vol1.pdf

За себя скажу так: я ее прочитал относительно недавно (весной этого года), и хотя на тот момент уже имел довольно большой практический опыт в программировании, все же узнал для себя много нового и интересного. Особенно полезным было написание и выполнение программ в терминале. И хотя вам может показаться, что лично вам это скорее всего не понадобится (особенно если вы прокачиваетесь во фронт, типа там терминал совсем не нужен), на самом деле многие вещи очень взаимосвязаны. Во фронте вы так же будете работать со строками, числами, массивами, объектами и т.п. И если вы будете лучше понимать как это работает в принципе, то и во фронте вам будет сильно проще и понятней. А если вы потом захотите расшириться до fullstack, то тем более все это вам понадобится.

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

Итак, вызывать извне команды мы будем посредством GraphQL-API. Для этого я написал небольшой резолвер и дал соответствующий метод. Вот результат:


Обратите сразу внимание на разницу того, в каком виде результат выводится в панели самого GraphQL Playground и в dev-tools. Особенно результат отличается во всплывашке (это я навел мышку в dev-tools на строку результата). Согласитесь, разница есть. Во всплывашке это больше похоже на то, что мы видели в самом терминале (то есть отформатировано, не имеет спецсимволов \n (которые есть суть знака переноса строки) и т.п.). То есть здесь мы сразу наблюдаем разницу между исходными данными и форматированными. Здесь надо понимать, что исходные данные - это просто одна строка. А форматированные - это обработанные программой для более понятного представления для простых пользователей. Это и есть суть UI, то есть пользовательские интерфейсы (User Interface). Но мы же программисты, там надо обработать эти данные и выполнять свою логику дальше. ОК, что мы с этим можем сделать? Как я и говорил, здесь есть спецсимволы \n, которые есть суть "перенос строки". То есть мы можем взять эту единую строку и разбить ее на массив отдельных строк, используя в качестве разделителя как раз этот спецсимвол \n.

Вот наша исходная строка:
str = ` Name Command State Ports -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- docker-nextjs_mail_1 MailHog Up 0.0.0.0:1025->1025/tcp,:::1025->1025/tcp, 0.0.0.0:8025->8025/tcp,:::8025->8025/tcp docker-nextjs_mysql_1 docker-entrypoint.sh mysqld Up 0.0.0.0:3306->3306/tcp,:::3306->3306/tcp, 33060/tcp docker-nextjs_pma_1 /docker-entrypoint.sh apac ... Up 0.0.0.0:8090->80/tcp,:::8090->80/tcp docker-nextjs_proxy_1 /bin/parent caddy --conf / ... Up 0.0.0.0:2015->2015/tcp,:::2015->2015/tcp, 0.0.0.0:2016->2016/tcp,:::2016->2016/tcp, 0.0.0.0:2017->2017/tcp,:::2017->2017/tcp, 443/tcp, 80/tcp `
Здесь я ее присвоил переменной str в консоли браузере. Чтобы было понятней, вот скрин:


Здесь мы опять не видим спецсимволы \n, так как консолька их скрывает, но надо помнить, что они там есть.


Есть и другие спецсимволы, такие как \t (символ табуляции), \r (перенос каретки. Работает практически так же как и \n, но все же есть отличия).

Так вот, сейчас я эту строку разобью на отдельные строки по знаку переноса. Использую для этого нативный метод split(). На выходе получу вот такой массив строк:


Здесь мы видим, что на выходе получился массив с 7 элементами, каждый из которых есть строка (напоминаю, что в массивах индексы элементов начинаются с 0. Элементов 7, но индексы их: 0, 1, 2, 3, 4, 5, 6). Но последняя строка пуская. Удалим ее, используя нативный метод filter(). В него я передаю в качестве аргумента функцию, которая будет применяться к каждому элементу этого массива для определения, нужен нам этот элемент или нет. Здесь функция простая: возвращает этот самый текущий элемент. Но он будет преобразован в истинность. Так как пустая строка при образовании к логическому дает ложь, то такой элемент будет пропущен. то есть "" == false, или Boolean("") === false


Как видите, здесь уже на выходе 6 элементов. Обратите внимание на синтаксис. Здесь нет присвоения результатов переменным. Здесь есть исходная строка, на ней вызывается метод .split("\n") и следом за ним вызывается метод .filter(n => n). Здесь надо понимать, что в результате выполнения каждого метода последовательно, следующий метод применяется к результату предыдущего метода. У нас была строка. К ней мы применили метод .split("\n"), в результате выполнения которого был получен массив, содержащий N строк. И вот к этому массиву (который никуда не был пока присвоен) мы применили метод .filter(n => n), в результате выполнения которого мы получили новый массив, содержащий строки, удовлетворяющие условию n => n. И далее по такой логике мы можем применять и другие методы, пока не получим конечный результат (что мы и будем делать далее).

Теперь у нас 6 строк в массиве. Но как мы видим, у нас первые две строки - это заголовки и разделитель. То есть нам они не нужны и их надо выкинуть из массива. Для этого используем метод .splice(2, 4). Здесь мы в текущем массиве взяли 4 элемента, начиная со второго и получили новый массив с этими элементами.

К слову, автор упомянутой книги, а именно Андрей Викторович Столяров, за такой стиль программирования меня бы наверняка сильно наругал. Говорю это точно, так как был у него на индивидуальном занятии и оно закончилось преждевременно :) Андрей Викторович такое называет "Сишность головного мозга" (по наименованию языка программирования C (Си))и совсем это не поощряет. Есть у него даже книга Оформление программного кода. Методическое пособие, в которой он дает рекомендации как правильней оформлять свой код, чтобы он был более понятный другим программистам (Ведь вполне вероятно ваш код будет читать кто-то другой). И вот этот самый код по идее должен быть написан как-то так:

var str = " Name Command State Ports \n--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\ndocker-nextjs_mail_1 MailHog Up 0.0.0.0:1025->1025/tcp,:::1025->1025/tcp, 0.0.0.0:8025->8025/tcp,:::8025->8025/tcp \ndocker-nextjs_mysql_1 docker-entrypoint.sh mysqld Up 0.0.0.0:3306->3306/tcp,:::3306->3306/tcp, 33060/tcp \ndocker-nextjs_pma_1 /docker-entrypoint.sh apac ... Up 0.0.0.0:8090->80/tcp,:::8090->80/tcp \ndocker-nextjs_proxy_1 /bin/parent caddy --conf / ... Up 0.0.0.0:2015->2015/tcp,:::2015->2015/tcp, 0.0.0.0:2016->2016/tcp,:::2016->2016/tcp, 0.0.0.0:2017->2017/tcp,:::2017->2017/tcp, 443/tcp, 80/tcp\n" var array = str.split("\n") var condition = function(item) { return Boolean(item) === true; } var array_filtered = array.filter(condition) var array_services = array_filtered.splice(2,4)

И на выходе получить переменную array_services, содержащую наш отфильтрованный массив.


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


Такое довольно объемное лирическое отступление получилось, но надеюсь, внес хоть немного ясности.

Возвращаясь к нашей задаче, напоминаю, что мы получили на выходе массив строк, каждая из которых содержит информацию об отдельном докер-контейнере. Но нам еще надо бы из каждой такой строки получить более подробную информацию. И вот здесь возникает сложность, потому что хотя мы и можем каждую такую строку разбить на отдельные части, но они не стандартизированы по количеству элементов. К примеру вот две строки:
docker-nextjs_mail_1 MailHog Up 0.0.0.0:1025->1025/tcp,:::1025->1025/tcp, 0.0.0.0:8025->8025/tcp,:::8025->8025/tcp
docker-nextjs_mysql_1 docker-entrypoint.sh mysqld Up 0.0.0.0:3306->3306/tcp,:::3306->3306/tcp, 33060/tcp

Как мы видим, между названиями служб и статусов (Up) у нас строки так же могут содержать пробелы. То есть мы заранее не можем знать сколько будет пробелов в команде и соответственно не можем просто так вычленить содержимое. Так что это практически тупик. На самом деле можно было бы еще поиграться и что-нибудь придумать (те же регулярные выражения заюзать). Можно было бы и вовсе заюзать команду docker-compose ps --services, которая возвращает именно список служб. Но в таком случае у нас кроме названия служб больше ничего и не было бы (а нужна более детальная информация). Соответственно, я начал смотреть какие более подходящие решения возможны. И тут наткнулся в документации, что разработчики докера теперь предлагают юзать не отдельную программу docker-compose (как мы это делали раньше и которую мы вызывали в наших примерах), а новый плагин для самого докера, который к программе docker добавляет команду (или подпрограмму) compose.

Устанавливается плагин так:
mkdir -p ~/.docker/cli-plugins/ curl -SL https://github.com/docker/compose/releases/download/v2.0.0-rc.3/docker-compose-linux-amd64 -o ~/.docker/cli-plugins/docker-compose chmod +x ~/.docker/cli-plugins/docker-compose
И вот в этой новой плагин-программе появляется новый параметр --format. И теперь вместо docker-compose ps мы можем выполнить docker compose ps --format json, чтобы получить результат в виде JSON-строки.

Обратите внимание, что это две совершенно разные команды, хоть и сильно похожие. В первом случае docker-compose ps - это программа docker-compose у которой мы вызываем команду ps, а во втором случае мы вызываем программу docker, у нее подпрограмму compose, а у той команду ps с флагом --format json
Даже вызов стправки на этих командах дает чуть разный результат.


И вот теперь мы на выходе имеем уже не просто строку, а JSON-строку.


И хотя это пока что все еще строка, теперь мы ее можем распарсить штатными средствами JSON.parse()


Как мы видим, в результате парсинга мы получаем уже нормальные объекты с кучей структурированной полезной информации.

А можно на стороне GraphQL-сервера указать этому полю тип JSON и сразу в ответе получать структурированные данные.


Вот так, по сути, работая с довольно базовыми на самом деле вещами, можно получить интересный результат. И даже если вы планируете работать только с React (или Vue, или еще с чем-то, не важно), вы все равно будете работать с массивами, строками, преобразовывать их и т.п. Да, на многое будут готовые компоненты, но если вдруг чего-то не найдется и придется писать самому, то будет вам большим плюсом писать свой собственный код.

P.S. Если кто захочет поиграться с этой наработкой, я вылил ее сюда: https://github.com/Fi1osof/low-code
Актуальный коммит для этой статьи: https://github.com/Fi1osof/low-code/tree/ee56e5bdb60ef0baca50cdfc5e0cd878f9709051

Только имейте ввиду, что для безопасности я сразу прописал для этой команды правило доступа isSudo, так что для ее выполнения надо быть авторизованным пользователем с флагом sudo, и это не системный пользователь, а именно в рамках текущего JS-проекта. То есть надо иметь запущенный MySQL-сервер и связь с ним. Все это описано в статье Запуск тестового проекта локально. Токен авторизованного пользователя надо указать в заголовке Authorization

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

Нет, не могу в двух словах описать. Слов и так много написано :) Важно не то, что можно будет мочь потом, а то, что можно мочь, умея работать со строками и массивами.
А что потом на выходе у меня получится - это совсем другой вопрос, там будет видно в процессе.

Добавить комментарий