Все-таки самоизоляция не проходит бесследно. Сидишь себе дома, а в голову разные мысли приходят. Как, чем осчастливить человечество? И вот оно: CDD! (И еще PDD / SOLID / KISS / YAGNI / TDD / Bootstraping...)
1. CDD - Cli Driven Development - Новый подход
Немного истории
Как-то поручили мне сделать Cli в одном нашем embedded устройстве. Разумеется, C/C++ (пусть будет C++, раз ресурсов хватает). Конечно есть много Cli-фреймворков.
Но я сделал свой вариант.
Для Linux можно использовать <termios.h> и получать коды символов после установки свойств терминала:
signal(SIGINT, SIGINT_Handler); // Ctrl+C
signal(SIGTSTP, SIGTSTP_Handler); // Ctrl+Z
int res_tcgetattr = tcgetattr(STDIN_FILENO, &terminal_state_prev);
terminal_state_new = terminal_state_prev;
terminal_state_new.c_lflag &= ~(ICANON | ECHO);
int res_tcsetattr = tcsetattr(STDIN_FILENO, TCSANOW, &terminal_state_new);
Для Windows можно использовать <conio.h>.
Добавляем немного классов, делаем список команд, и добавляем команды по типу:
{
Cli_Command_Abstract_t *cmd = new Cli_Command_Abstract_t(Cli_Command_ID_help);
cmd->Add(help_keyword);
cmd->Help_Set("show this help, \"help full\" - show all available commands");
command_tree->Add(cmd);
}
И все-бы ничего, пока команд 10-20. Ну пусть еще help / quit / debug cli (типа очень нужная команда - об этом позже). Интересно, что основной функционал уложился в 20 команд, а вот разные обвязки… Управление SNMP / Syslog / NTP / Users / FTP / SSH / VLAN … и у нас - 250 команд. Ух ты! Начинаются проблемы с монолитным приложением, и очень хочется разбить все на модули, желательно попроще и поменьше. И вот отсюда и начинается CDD - Cli Driven Development.
1.1 Использование Cli в различных типах приложений
Вообще, Cli, не смотря на GUI, используется во многих типах приложений: САПР, игры, базы данных, среды выполнения (Erlang, Lua и др.), IDE. Можно утверждать, что включение консоли могло бы сделать многие приложения более удобными (например, можно представить Paint с командной строкой: количество команд невелико, VBA будет лишним, но одна лишь возможность выполнения скриптов могла бы значительно изменить работу с программой).
1.2 Введение в CDD
Cli-интерфейс жив и развивается. Cisco-like - это вполне вполне рабочий термин.
Что же может современный Cli? - Довольно много:
развитую систему команд с выводом подробной информации, в том числе об аргументах команды;
группировку команд ("уровни");
задание группы объектов для управления ("параметры");
логгирование;
исполнение скриптов;
типизированный ввод данных с валидацией;
Я придумал еще одну функцию: debug cli - проверка команд (CMD_ID / CMD_Item / CMD_Handler)
может показать число ID ("задуманные команды"), Realized- и NotRealized-команды для каждого модуля; (В идеале счетчики ID, Realized должны быть равны, но если NotRealized не равен 0, то это еще один стимул для разработчика: ну осталось всего-то 30...20...5...2 нереализованных команд - неужели оставим так? может лучше доделать? - и это работает!)
1.3 Основные идеи CDD
Можно сформулировать основные идеи CDD:
Если есть какой-то функционал, должны быть Cli-команды, обеспечивающие управление этим функционалом и команды получения информации о данном функционале. Не должно быть функционала без Cli-команд.
Модульное построение: любой модуль можно убрать или заменить с предсказуемыми изменениями в функционале.
Модули связываются только на самом верхнем уровне: все связи должны легко прослеживаться (фактически мы пользуемся тем, что приложений с полной связностью не существует / не может быть / мы должны избегать).
1.4 mCli - Реализация CDD
CDD использовано при построении mCli - Cli-фреймворка модульного типа (github.com/MikeGM2017/mCli). В текущем состоянии имеются события, типы и модули.
1.4.1 События mCli
В простейшем виде для ввода с клавиатуры нужно определение кода нажатой клавиши и (отдельно) определение нажатия Enter (ввод команды) и Ctrl+C (прерывание команды). В полном наборе необходимо определение нажатия Enter (ввод команды), Ctrl+C (прерывание команды), Up/Down (просмотр истории команд), Left/Right/Home/End (перемещение по строке ввода), Back/Delete (изменение строки ввода).
1.4.2 Типы mCli
mCli предполагает использование типов при вводе данных. В текущей реализации имеются следующие типы:
Word / Word_List / Word_Range (ключевые слова, List - можно ввести несколько ключевых слов через запятую, Range - выбор одного ключевого слова из нескольких вариантов)
Int / Int_List / Int_Range
Str
IP4 / IP6
MAC
Date / Time / DateTime
EQU_Range ( == != > < >= <= - для использования в скриптах, условное выполнение)
Rem (комментарий - для использования в скриптах)
1.4.3 Модули mCli
Модули mCli можно разделить на базовые, платформо-зависимые и кастомные.
Базовые модули:
Base_Quit (выход из приложения)
Base_Help (вывод информации по командам и их аргументам)
Base_Modules (вывод информации по задействованным модулям)
Base_History (история команд)
Base_Script (выполнение скриптов)
Base_Rem (комментарий, для использования в скриптах)
Base_Wait (пауза, для использования в скриптах)
Base_Log (управление логом)
Base_Debug (проверка списка команд, определение нереализованных команд)
Check (условное выполнение, для использования в скриптах)
Платформо-зависимые модули
Вывод:
Output_printf (Linux/Window)
Output_cout (Linux/Window)
Output_ncurses (Linux)
Output_pdcurses (Linux/Window)
Ввод:
Input_termios (Linux)
Input_conio (Window)
Input_ncurses (Linux)
Input_pdcurses (Linux/Window)
Кастомные модули:
ConfigureTerminal (демо: тестирование переменных)
SecureTerminal (демо: вход в модуль по паролю)
TestTerminal (демо: тестирование типов)
1.5 Объединение модулей в mCli
Связывание модулей происходит на самом верхнем уровне, например в функции main():
Cli_Modules Modules;
// Modules Add - Begin
Modules.Add(new Cli_Module_Base_Rem(Str_Rem_DEF, Cli_Output));
bool Cmd_Quit = false;
Modules.Add(new Cli_Module_Base_Quit(Cmd_Quit));
Str_Filter str_filter('?', '*');
Modules.Add(new Cli_Module_Base_Help(User_Privilege, Modules, str_filter, Cli_Output));
Modules.Add(new Cli_Module_Base_Modules(User_Privilege, Modules, str_filter, Cli_Output));
Cli_History History;
Modules.Add(new Cli_Module_Base_History(History, Cli_Output));
Modules.Add(new Cli_Module_Base_Log(Cli_Input));
bool Cmd_Script_Stop = false;
int Script_Buf_Size = 1024;
Modules.Add(new Cli_Module_Base_Script(History, Cli_Output,
Str_Rem_DEF, Cmd_Script_Stop, Cmd_Quit, Script_Buf_Size,
CMD_Processor));
bool Log_Wait_Enable = true;
bool Cmd_Wait_Stop = false;
Modules.Add(new Cli_Module_Base_Wait(Log_Wait_Enable, Cmd_Wait_Stop, Cli_Input, Cli_Output));
Modules.Add(new Cli_Module_Test_Tab_Min_Max());
Modules.Add(new Cli_Module_Test_Terminal(Cli_Input, Cli_Output));
Modules.Add(new Cli_Module_Base_Debug(User_Privilege, Modules, Levels, CMD_Processor, Cli_Output));
Modules.Add(new Cli_Module_Check(Modules, Values_Map, str_filter, Cli_Output, Cmd_Script_Stop));
// Modules Add - End
1.6 CDD и SOLID
SOLID в CDD достаточно легко обнаружить на уровне подключения и объединения модулей. Какие-то модули практически всегда используются, например Cli_Output нужен в большинстве модулей. Другие - гораздо реже (например, Cli_Input нужен только в модулях, в которых команда требует подтверждения).
Таким образом, SOLID в CDD - это:
S - каждый модуль отвечает за свой круг задач
O - здесь есть проблема: в каждом модуле есть enum Local_CmdID, и получается, что при наследовании список Local_CmdID не так просто расширить? Но в новом модуле мы можем завести новый enum Local_CmdID или (лучше) можно ввести новый enum Local_CmdID только для новых команд, стартующий с последнего элемента предыдущего enum (для этого можно использовать CMD_ID_LAST)
L - модуль может быть заменен на другой, с доработанной реализацией
I - при замене модуля может возникнуть ситуация, что потребуется больше (или меньше) связанных модулей; при создании экземпляра модуля это легко учесть (через конструктор или статический инициализатор)
D - модули связываются на верхнем уровне
1.7 CDD и KISS
В целом, набор команд любого модуля стремится к расширению: более удобный формат команд, новые флаги, новые возможности. Но сам список команд всегда доступен, и, если он сильно разрастается… то просто делим модуль на 2 или более модуля!
На уровне модуля команды могут различаться флагами или дополнительными параметрами. Но реализация у них может быть одна. Например, "help" и "help full" реализуются одним методом, в качестве параметра принимающий строку фильтра - "*". Так что KISS сохраняется в таком смысле:
команда выполняется методом, имеющим несколько флагов (да, из-за этого метод делается чуть сложнее, зато несколько команд Cli могут выполняться однотипно).
1.8 CDD и DRY
Повторяющийся код выносим в отдельный класс, создаем объект и ссылку на него используем при создании модуля.
1.9 CDD и YAGNI
Нужно убрать какой-то ненужный функционал? - Убираем ненужный модуль (или команды в модуле). За счет слабой связности модулей это несложно.
1.10 CDD и Bootstraping
В некоторых случаях (например, Embedded Baremetal) у нас есть только консоль. CDD может быть применено для разработки приложения "с нуля".
1.11 CDD и TDD
За счет наличия скриптов и модуля условного исполнения автоматизация тестирования сводится к следующему сценарию:
вручную вводится последовательность тестируемых команд;
история команд сохраняется в файле скрипта;
при необходимости скрипт редактируется / дополняется проверкой правильности выполнения;
вызов скрипта добавляется в общий скрипт тестирования или используется сам по себе;
1.12 CDD и GUI
А что GUI? GUI (да и Web - тоже) пусть посылает текстовые команды в Cli - эстетично, наглядно, надежно.
2. CDD и PDD
А вот еще и PDD!!!
2.1 PDD - Provocation Driven Development - еще один новый термин :)
Вообще, PDD - это то, что нас настигает постоянно. Допустим, есть путь, по которому мы идем к цели. Но на что нас провоцирует этот путь? Считаю, что мы должны осознавать это. Например, на что провоцируют языки программирования:
C провоцирует на нарушения доступа к памяти и на плохо контролируемые приведения типов;
C++ - на создание монолита (если за этим не следить, то имеем типовой пример: class MyCoolGame; myCoolGame.Run());
SQL, Lua - "все есть таблица";
Assembler - "стандартов нет";
Java - "щас понаделаем объектов";
JavaScript - "щас наподключаем библиотек, не самим же все делать"; … и так далее - дополнительные примеры каждый, думаю, сможет придумать.
2.2 Что есть PDD для CDD?
В первую очередь - это тенденция на разбиение проекта на модули. Действительно:
Есть объект управления? - Выносим в модуль.
Есть повторяющийся код? - Выносим в модуль.
Новый функционал? - Добавляем новый модуль.
Новая архитектура? - Заменяем модули.
Описание команд - это текстовое описание функционала, фактически мы получаем DSL. Чтобы получить информацию о доступном функционале, достаточно ввести команду "help".
Предсказательный характер архитектуры:
пусть в расчетах на каждую Cli-команду отводим 1 (один) человеко-день. Да, можно за 1 день ввести 10-20 простых Cli-команд (да, простые или однотипные команды реализуются быстро), но не нужно обманываться: будет (обязательно будет!) функция, которая потребует 10 дней на реализацию и тестирование. Поэтому проект средней сложности на 200-300 Cli-команд займет 200-300 человеко-дней (хотя, это скорее оценка "сверху", реально проект может быть закончен раньше).
Скрипты с возможностью условного исполнения означают встроенную возможность тестирования, что (в теории) уменьшает вероятность регресса.
Расширяемость: новый модуль может, конечно, добавлять новые команды в глобальную видимость, но предпочтительный путь - группировка команд на уровне модуля, тогда в глобальной видимости появится всего лишь одна команда - переход на "уровень" модуля.
В Cli достаточно легко задавать обработку группы объектов. Можно, например:
ввести список объектов в команду;
ввести фильтр по именам объектов в команду;
ввести список объектов как параметр;
или даже ввести модуль Selection для управления группировкой объектов. Таким образом, можно использовать различные варианты групповой обработки.
Приложение достаточно легко поддерживать в постоянно рабочем состоянии: да, какой-то функционал еще не введен, зато все остальное - работает.
По-видимому, CDD ортогональна другим подходам, так как CDD может быть применено как в Waterfall, так и в Agile. Действительно, ввод сначала новых команд, а потом их реализация - близок к Waterfall. Но модульное построения и малая связность модулей способствуют применению Agile. Но CDD не мешает ни Waterfall, ни Agile, а только помогает.
3. Встроенный язык скриптов
3.1 Модуль Check
Условное выполнение реализовано в модуле "Check".
Для условного выполнения команд, в принципе, достаточно всего двух команд: "check label " - установка метки "check if == goto " - условный переход (здесь сравнение может быть не только на равенство: == != > < >= <= - вот полный список, но при этом команду можно оставить одну и ту же, а операторы сравнения ввести в виде списка возможных значений)
Переменные в простейшем случае - глобальные, заносятся в map<string,string>, для чего в модуле предусмотрен виртуальный метод .To_Map().
Для работы с переменными введены команды условного и безусловного присвоения, объединения, вывода на экран. Для полноценного языка этого, возможно, мало, но для задач тестирования функционала - вполне приемлемо.
3.2 Модуль Check vs Lua
Да, вместо встроенных модулей скриптов и условного выполнения можно подключить Lua. Однако, вместо нескольких команд (в действительности модуль условного выполнения Check получается не такой уж маленький - более 30 команд, хотя и однотипных) подключение Lua означает большое увеличение размера исполняемого файла, а в некоторых случаях это может быть критичным. Но как вариант, Lua выглядит очень привлекательно.
3.3 Модуль Check vs Erlang
Было бы неплохо, но запросы… Уж очень большие у Erlang требования к ресурсам. Возможно, на "жирных" устройствах, как вариант, можно и Erlang подключить.
4. CDD vs Erlang
Неплохая попытка, подход Erlang - довольно похож на CDD. Но задумаемся, в чем PDD для Erlang? - "Ошибаемся и еще раз ошибаемся, а система все равно работает". Это, конечно, сильно. Поэтому вопрос: "CDD или Erlang" безусловно стоит. Но CDD можно реализовать на многих языках программирования (C/C++, C#, Java, JavaScript). А у Erlang - очень специфичный подход. Может быть, не Erlang vs CDD, а Erlang + CDD ??? Кажется, надо попробовать...
5. CDD и дробление монолита
Примерный путь преобразования монолита в CDD-приложение:
создаем CDD-приложение из Base-модулей;
legacy-монолит добавляем в виде нового Cli-модуля на новом "уровне" с минимальными командами вида "version get" / "info get" - на первом этапе достаточно "установить контакт" с монолитом;
в новом модуле вводим команды, специфичные для него: "start" / "stop" / "configure" …;
скорее всего новые команды будут группироваться вокруг каких-то понятий / объектов / процедур и т. п. - это повод выделить такие группы в отдельные модули + объекты управления; при этом в основном монолите вводятся ссылки на выделенные объекты;
в результате должен получиться набор модулей, причем каждый модуль должен содержать не более 10-20 команд;
когда монолит разбит на модули, можно изменять модули по отдельности, заменять их, вводить новый функционал, добавлять модули и т. д.
6. Итоги
CDD выполняет SOLID, KISS, DRY, YAGNI, Bootstraping, TDD.
CDD провоцирует на модульное построение.
CDD дает возможность выполнения скриптов и внутреннее тестирование.
CDD может быть основой большого количества типов приложений.
CDD позволяет вводить новый функционал прогнозируемым способом.
CDD может быть основой построения OS.
CDD может быть использовано для относительного удобного поглощения монолитов. Причем вполне возможна оркестрация как монолитов, так и сервисов.
CDD дает возможность разделения работ:
постановщик задачи описывает новый модуль в виде набора команд;
исполнитель реализует команды;
тестировщик пишет скрипты для проверки нового функционала.
CDD поддерживает введение нового функционала, в том числе на разных уровнях:
новые модули;
новые команды в существующих модулях.
CDD обеспечивает безопасность при вводе команд:
команды парсятся, данные валидируются, сделать что-то вне Cli-команд невозможно (если, конечно, не вводить команды типа exec / system / eval).
CDD фактически дает документацию по функционалу приложения:
достаточно подать команду "help * verbose" - и описание команд и их аргументов уже есть.
Этого мало?
Тогда вот вам напоследок: CDD позволяет захватить мир. КМК …
Да, и Linux стоит переписать по CDD. КМК
Cheater
Вроде не первое апреля? Вы переизобрели шелл, включая bind/bindkey, restricted shell и т.д.
MikeGM2017 Автор
Да, но как бы нет.
Из песочницы трудно попасть в нужный день, но давайте считать, что эта серьезная статья приурочена к 1 апреля :)
Но предлагаю в этой шуточной статье выделить 2 ключевых момента:
1. Cli может присутствовать в широком спектре приложений
2. Cli должно быть модульным (что лишает статуса CDD многие Cli-фреймворки)
Поэтому предлагаю ввести формулу для CDD:
1. Cli + Modules (без модулей сразу получается монолит)
2. Cli + Scripts (потому что скрипты сделать довольно просто)
3. Scripts + Conditional Execution (дает встроенное тестирование)
Таким образом:
CDD = Cli + Modules + Scripts + Conditional Execution
То есть:
CDD = CMSCE