Технология софтостроения NexusMind (Или как я строил TELEGRAM бота на python)

Данная статья касается вопросов «софтостроения» (термин этот я подсмотрел у своего коллеги и друга Сергея Тарасова). Термин мне нравится. Всегда встаю в тупик по порядку изложения. В первую очередь сказать о техническом задании (ТЗ) или о теоретических аспектах или продумывании как и что реализовать в программном коде? Ведь очевидно, что прежде, чем что‑то писать программист должен хотя бы минуту подумать, что он напишет? Есть и другие варианты написания, но мы их касаться не будем и начнем все‑таки с ТЗ.

Вкратце ТЗ звучало так. Написать информационно‑справочную систему поиска и бронирования лекарственных препаратов с использованием телеграмм бота (ТБ) для сети аптек на основе 1С конфигурации.

Задача была написана на 1С и работает (или работала) под телеграмм ботом с именем @lotostatki_bot. Это не предмет статьи, но прототип. Почему 1С? Потому, что 1С это учетная система, а бронирование товара — это документ, который поступает в аптеку и резервирует товар на определенное время. Схема, по которой ведет диалог ТБ @lotostatki_bot следующая:

Схема ТБ для 1С

Схема ТБ для 1С

Все оказалось медленней, чем хотелось бы и возникло решение переписать задачу на основе микросервиса на python, убрав 1с и апачи. То есть перейти к схеме

Схкма ТБ на python

Схкма ТБ на python

Сам микросервис строится на основе двухзвенки. То есть база данных и клиент работают в паре без какого‑либо сервера приложений. Поскольку источник данных представляет реляционную базу и принадлежит третьей стороне, то что‑то изменять в источнике данных было запрещено. Select * from бла‑бла только. Поэтому введение в микросервис собственной базы вполне оправдано. В качестве системы управления базами данных (СУБД) возьмем MySql (взята эта платформа в силу ее бесплатности и слабости по сравнению с другими СУБД). Если на маломощной СУБД можно будет реализовать микросервис, то и на более мощных СУБД все пойдет «на ура».

Микросервис работает через слой хранимых процедур MySql, который берет на себя генерацию динамических SQL предложений и ряд других функций по формированию связей. Как ни странно, но использование хранимых процедур — это редкость для современных систем (я не знаю современной системы, которая бы использовала всегда хранимые процедуры вместо прямого доступа к данным select, insert, update, delete). В плане хранимых процедур, многое взято из системы UltimaS (клиент версии 247) или Nexus (клиент версии 252), в которых обработчики классов были хранимые процедуры, а данные хранились в виде дерева. Конечно, «хранение данных в виде дерева в публичной таблице» было ошибкой, но ошибкой, как ни странно, правильной при обработке данных в персональной таблице. Видимо, еще существуют ссылки на нее на опенсорсных ресурсах, а может быть этих ссылок уже и не найти.

Здесь собственно оканчивается преамбула и начинаются размышления о том, что будет положено в основу построения ТБ. Я пользуюсь определениями Дж.Мартина из книги «Организация баз данных», 1978, где автор говорит о трех типах баз данных: реляционной, иерархической (дерево) и сетевой. Нас будет интересовать иерархическая база данных или «дерево ветвями вниз». Такое представление данных еще называют структурой связей «parent‑child».

Утверждаю, что «Любую реляционную таблицу можно представить в виде дерева и наоборот». Покажем на примере, как работает данное утверждение. Сконструируем метаданные дерева или иерархии, с которой будем работать, по некоторому прошлому опыту:

CREATE TABLE `6478540544:aaew_jd-oj0ghd-9s2md1hze-x6iv6yacvc_1209365803` (

# имя таблицы строится как конкатенация ИДбота, underscore и ИДпользователя телеграмма

            `Чат` text NOT NULL,                         # идентификатор чата или беседы

            `Узел` text NOT NULL,                       # объект, поскольку диалог, то тип поля текстовый.

            `Класс` text NOT NULL,                     # название столбца реляционной таблицы

            `Родитель` bigint(20) NOT NULL,       # uon папки объекта

            `СтарРод` bigint(20) DEFAULT '0',      # uon папки объекта предидущий

            `Код` text NOT NULL,                        # код объекта

            `Отм` bit(1) DEFAULT NULL,            # отметка или отбор

            `вверх` text,                                         # адрес объекта сверху вниз

            `вниз` text,                                          # адрес объекта снизу вверх

            `uon` bigint(20) unsigned NOT NULL AUTO_INCREMENT, #Unique Object Number

);

Иными словами, каждый пользователь телеграмм будет иметь свою персональную таблицу, чтобы избежать каких‑либо блокировок со стороны работы других пользователей. Обобществление данных из персональных таблиц в этой статье не будет рассмотрено. Такое решение можно реализовать только средствами динамического SQL. Логика и генерация динамического SQL будет реализована в рамках СУБД в слое хранимых процедур. В конечном итоге, забегая вперед, этот слой хранимых процедур может быть модифицирумым и функционально зависимым от ответов диалога телеграмм бота.

Наименования столбцов из реляционной таблицы будут представлять «Класс» узлов, а их порядок в select уровни дерева сверху вниз. Например, запрос к реляционной таблице

Select Colour, Wide, Height from Box  

И ее резалтсет:

Black               110      100

Yellow               90        80

Black                90        80

Yellow             110      100

Данный Select будет иметь дерево с именем Box, у которого первый уровень будет Colour, второй ‑Wide, и третий Height. А расположение значений из резалтсета из горизонтального превратятся в вертикальное. Назовем такую операцию транспонированием.

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

Схема транспонирования реляции в дерево

Схема транспонирования реляции в дерево

Операция Отбора или Выбора деревьев

Эта операция отмечает или выбирает значение узла определенного класса дерева по всем существующим деревьям, а не только в определенном дереве, где это значение выбирается. Например, мы выбрали Ширину = 110 в некотором дереве грузов. Тогда операция выбора осуществляет отметку и во всех других деревьях, на данный момент присутствующих. Иными словами под груз с шириной 110 сразу отберется ветвь с шириной=110 в дереве бокс. И можно сразу сказать, что готовый к перевозке груз поедет в черной или желтой коробке. Понятно, что выбор многообразнее, но данный упрощенный пример показывает саму идею отбора решения на множестве деревьев.

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

 Операции переноса и возвращения переноса деревьев

Деревья, у которых Родитель=0 будем называть активным уровнем. В активную зону будут перемещаться деревья, которые требуются для построения нового класса. Например для класса «бронь» потребуется цепочка классов «шаблон, пользователь, товар, аптека, количество брони». Только тогда бронь может быть сформирована и отослана в аптеку. Копирование деревьев запрещено. Все деревья присутствуют в оригинале и перемещаются в системе путем изменения значения родителя. При этом значение первоначального родителя всегда сохраняется в поле «СтарРод».

 Операции забытья и обратная вспоминания деревьев

Определим максимальный уровень запоминания как N, который будет определять глубину памяти. Поэтому «забыть» означает просто опустить все деревья на один уровень, а вспомнить — это поднять все деревья на один уровень.

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

Смотрим на схему базы данных, которая получилась,

Метаданные базы

Метаданные базы

Хранимые процедуры пока преобладают и написаны однотипно с использованием динамического SQL.

Вид хранимых процедур

Вид хранимых процедур

Разработка программы на питоне значительно упрощается за счет использования слоя хранимых процедур. Программа вызывает хранимые процедуры только. В качестве редактора был использован Notepad++. И ничего более. Отладка велась в ручном режиме с использованием print ().

python code

python code

Запускаем микросервис.

Микросервис ждет

Микросервис ждет

Как это работает?

Отладка в процессе работы

Отладка в процессе работы

Что происходит в базе? В активной зоне появляются два дерева «пользователь» и «шаблон»

Активная зона

Активная зона

Микросервис успел обратиться к базе данных‑источнику и транспонировать в виде дерева все, что вытащил из источника

Результат транспонирования реляции в дерево

Результат транспонирования реляции в дерево

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

Обработка диалога микросервисом

Обработка диалога микросервисом

В базе отметка всех узлов идет по всем деревьям синхронно.

Сквозная отметка по всем присутствующим деревьям

Сквозная отметка по всем присутствующим деревьям

Наконец в диалоге появляется аптека и товар, которые требуются для создания брони.

Выбор аптеки и товара

Выбор аптеки и товара

Они переносятся в активную зону.

Перенос в активную зону

Перенос в активную зону

Как образуются новые классы? Например, класс «бронь». Чтобы его создать необходимо подготовить набор из пользователя, товара, аптеки и количества брони. Как только какое‑то значение из перечисленного определяется, мы переносим его дерево в активную зону (АЗ). Приятная агрегатная функция

select GROUP_CONCAT (`Класс` ORDER BY `Класс` SEPARATOR ',')

from tlg.`6 478 540 544: AAEW_jD‑oj0ghd-9s2MD1hZe‑X6iV6YacVc_767 126 670`

where Родитель=0;

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

Цепочка классов

Цепочка классов

Как только введено количество, система способна создать «бронь»

Фиксация нового экземпляра класса

Фиксация нового экземпляра класса «бронь»

Цепочка классов и отослать сообщение с бронью в 1С через HTTP сервис.

ПОСТ запрос

ПОСТ запрос

После фиксации и отсылки брони система диалога возвращается в исходное состояние. Все перемещенное в активную зону возвращается по своим веткам. Как результат, у брони Родитель=uon с присвоение данных брони.

Диалог в исходное состояние

Диалог в исходное состояние

И

Класс

Класс «бронь» зациклен на себя как отработанная цель

В этом детальном разборе я старался показать, что основная цель программы — это построение связей или деревьев. Программист при таком подходе программирования не выходит за рамки манипулирования деревьями. Иными словами, технология софтостроения NexusMind — это и есть манипуляции со связями «parent‑child» в лесе деревьев классифицированных данных при оперативном выполнении программы. Цель не написать код программы, но создать новую связь или новую цепочку или новое дерево. Техника манипулирования деревьями, которая мной названа NexusMind, придумана мной не сейчас, а в 2005 году, когда я работал над новой версией 252 клиента Nexus для учетной системы UltimaS. Тогда я не понимал, как разделить работу клиента и цели техники. Все представляло кучу С++ кода и кучу MSSQL запросов. Реализация техники NexusMind на Python вычленило то, что относится к технике и, что является простым исполняемым кодом. Поскольку код предназначен выполняться только, то в нем отсутствует СМЫСЛ (имею в виду смысл с большой буквы, который иногда появляется в голове у чела). Для СМЫСЛА необходимы покой и тишина, непротиворечивость и полнота. Иными словами постоянство связей. Только тогда зарождается смысл. Поэтому применительно к построению программ необходимо максимально разделить процесс выполнения программы и осмысления или поиска новых связей или классов, которые должны быть непротиворечивы в плане ранее созданных классов и цепочек. Понятно, что смысл не есть что‑либо застывшее. Всегда хочется заглянуть за границы собственного смысла, связав ранее несвязанное.

© Habrahabr.ru