Иногда лучший выход из сложности кода — углубиться.
Вы, дорогой читатель, когда-нибудь попадались в ловушку настройки?
Пример 0: вы добавляете пользовательскую проверку диапазона в поля ввода с диапазонами, определенными в базе данных. Люди в восторге. Пока они не захотят, чтобы диапазон входных данных был условно определен другим полем.
Пример 1. Вы создаете страницу настроек, которая загружается с разными настройками для бесплатных и премиум-аккаунтов. «Отлично, это именно то, что мы хотели». До тех пор, пока, конечно, некоторые настройки не должны отображаться в зависимости от типа или типов устройств, на которых у пользователя есть активные сеансы.
Я мог бы продолжать до бесконечности. Дело в том, что чем более гибким вы или ваша команда делаете свой интерфейс, тем больше гибкости требуется. Особенности порождают больше возможностей. Может быть, Детская книга «Если дать мышонку печеньку» на самом деле была о расползании функций и раздувании программного обеспечения.
И это то, что вызывается в вашей когда-то простой, элегантной кодовой базе: раздувание. Возможно, вы могли бы написать кучу разных компонентов пользовательского интерфейса и переключаться между ними. Или вы можете заполнить один пользовательский интерфейс таким невероятным количеством особых случаев, что он станет неработоспособным. Какой бы яд вы ни выбрали, вы должны его проглотить.
Контекстно-управляемые языки сценариев
Если вы прочитали название, я предполагаю, что вы усвоили тезис: контекстно-управляемый язык сценариев может сделать ваш код более гибким, сократив при этом его раздувание и сложность.
Но что такое контекстно-управляемый язык сценариев (CDSL)? Что ж, это термин, который я придумал (это священное право всех разработчиков программного обеспечения придумывать термины). Он определяется так:
Контекстно-управляемый язык сценариев (CDSL) — это небольшой язык, обычно не полный по Тьюрингу, которому предоставляется доступ к предварительно определенному набору значений (контексту) для создания выходных данных.
Пример 0 с CDSL
Таким образом, CDSL может иметь доступ к значениям каждого поля на странице настроек, которые он использует для вывода сообщения об ошибке проверки диапазона или пустой строки, если текущее поле допустимо. CDSL берет этот код форматирования, который ранее был интегрирован в остальную часть пользовательского интерфейса, и помещает его в другое место. Вероятно, база данных. Давайте посмотрим на преимущества, которые это дает вам.
- Читаемость кода. Исключив эту логику проверки из кода внешнего интерфейса, вы сможете устранить множество особых случаев, которые мешают разработке внешнего интерфейса.
- Гибкость. Помните, что в примере 0 мы ранее добавили код для проверки диапазона, когда появилось новое требование, нам пришлось бы либо переписать старую систему, либо создать параллельную систему. При использовании CDSL единственный код, который изменяется, напрямую соответствует поведению поля, которое вы меняете.
- Повторное использование кода.После того как интерпретатор для вашего CDSL установлен, исполняемый им код можно использовать на нескольких поддерживаемых вами платформах. Изменение кода проверки для нашего поля изменяет его в разных интерфейсах, которые мы поддерживаем: мобильные устройства, Интернет и т. д. Мы также можем использовать тот же код для нашей вторичной проверки на сервере. Обычно требуется обеспечить проверку на уровнях пользовательского интерфейса и API.
- Быстрое развертывание.Поскольку логика проверки больше не является частью вашей кодовой базы, вы можете обновлять поведение приложений за пределами версионного развертывания. Для приложений для телефонов это означает обход процессов утверждения Apple и Android.
Написание контекстно-зависимого языка сценариев
Практически невозможно говорить о написании скриптовых языков вообще. Это больше подходит для учебника, чем для статьи. Давайте тогда обсудим один CDSL в частности. Чтобы написать этот CDSL, нам нужно придерживаться нескольких принципов.
Принципы нашей CDSL
Префиксная запись. Иногда называемая польской записью, префиксная запись очень проста в обработке. В префиксной записи операторы стоят перед операндами.
Eg: 3+4/5 turns into +3/45.
Обратите внимание, что префиксная нотация не имеет порядка операций. Это одно из больших упрощений, которое оно нам дает. Обозначение префикса также можно рассматривать как обозначение функции.
Без пробелов. Наш скрипт должен быть написан без пробелов и с минимальным количеством управляющих символов. Это поможет нам писать небольшие сценарии, особенно когда необходим только один условный оператор.
Чистая функциональность. Наши скрипты не должны изменять какое-либо состояние. Следовательно, мы должны сделать язык чистым функциональным языком.
Синтаксис нашего CDSL
Нам нужно установить синтаксис для нашего языка, позже нам нужно будет разобрать этот синтаксис и токенизировать наш скрипт. Люди размазали термин токенизация по другим разделам информатики, поэтому, возможно, лучше будет назвать это лексическим анализом. В любом случае мы будем выполнять обе задачи определения и токенизации с помощью регулярных выражений. Предупреждение. Если у вас проблемы с регулярными выражениями (перестаньте врать себе, у вас действительно проблемы с регулярными выражениями), ознакомьтесь с Regex101. Я написал об этом статью здесь.
Операторы
? <condition> <true return> <false return> | If-Else Statement = <left hand side> <right hand side> | Equality ! <argument> | Not > <left hand side> <right hand side> | Greater Than < <left hand side> <right hand side> | Less Than + <left hand side> <right hand side> | Addition - <left hand side> <right hand side> | Subtraction * <left hand side> <right hand side> | Multiplication / <left hand side> <right hand side> | Division % <left hand side> <right hand side> | Modulus (Remainder) | <left hand side> <right hand side> | Logical Or & <left hand side> <right hand side> | Logical And Note: Logical operators will treat any non-zero number as zero, there is no boolean number type.
Типы данных
Целые числа: этот CDSL будет иметь только целые значения. Если вам нужны поплавки при реализации собственного, то так тому и быть. На данный момент подойдут целые числа. Мы будем предварять все наши целые числа символом # для их обозначения.
#[-]?[0-9]+
Строки. Если мы собираемся решать проблемы, подобные указанным в нулевом примере, нам потребуется вывести строковые значения. Простая нотация с двойными кавычками подойдет. Мы можем избежать двойных кавычек с помощью обратной косой черты.
"((\\[\\,"])|[^",\\])*"
Интерполированные строки. Если мы хотим использовать наши скрипты для создания строк, интерполяция может сделать код более кратким. Поэтому мы будем выполнять интерполяцию строк, используя следующий синтаксис.
"String with arguments {1}, {2}, {1} and {3}" #1 "insertMe" #-1 Produces: String with arguments 1, insertMe, 1 and -1
Числа не должны следовать строгой натуральной последовательности чисел, но будут приниматься в качестве аргументов от наименьшего к наибольшему. Если мы добавим это в наше регулярное выражение, мы получим следующее:
"((\\[\\,",{,}])|[^",{,},\\]|({[0-9]+}))*"
Функции
Нам не нужна возможность определять функции внутри CDSL, но это не означает, что нам никогда не понадобятся предопределенные функции вне наших операторов. Обозначим их знаком «~» перед буквенными символами.
~[A-Za-z]+
Значения контекста
Мы говорили, что это контекстно-зависимый язык, не так ли? Конечно, нам нужен способ доступа к этому контексту внутри кода нашего скрипта. Это также самая нестандартная часть языка.
Скажем, у вас есть объект настроек, к полям которого вы хотите получить доступ. Я бы получил к нему доступ с помощью нотации s.fieldName. Детали будут зависеть от того, чего пытается добиться ваш CDSL.
Мы сохраним наше определение гибким. Один символ нижнего регистра, за которым следует точка и буквенное имя «контекста», к которому мы пытаемся получить доступ.
[a-z]\.[a-zA-Z]+
Существует проблема с регулярным выражением выше. Он жадно захватывает буквенные символы и начинает с буквенного символа. Это означает, что если мы поместим два из них рядом друг с другом, мы захватим префикс второго контекста с именем первого. Это нарушает наш принцип отсутствия пробелов.
Нам нужно добавить отрицательный просмотр вперед, чтобы не захватывать символ, за которым следует точка. Это должно быть добавлено к нашему регулярному выражению функции для той же цели.
([a-z]\.[a-zA-Z]+(?!\.)) - Context Values (~[A-Za-z]+(?!\.)) - Functions
Собираем все вместе
Теперь нам просто нужно объединить наши операторы регулярных выражений из операторов, типов значений, функций и контекстов. В итоге мы получаем ужасно длинное регулярное выражение, но такое, которое может разбить наш скрипт на строки токенов за один проход.
([\?,\=,!,<,>,\+,\-,\*,\/,%,\|,\&])|(#[-]?[0-9]+)|("((\\[\\,",{,}])|[^",{,},\\]|({[0-9]+}))*")|(~[A-Za-z]+(?!\.))|([a-z]\.[a-zA-Z]+(?!\.))
Создание двигателя
Теперь, когда у нас есть четко определенный синтаксис, нам нужно средство преобразования этого синтаксиса в вывод. Назовем это Scripting Engine. Я не знаю, называет ли это кто-то еще, но они должны.
Обратите внимание, что это будет очень простое описание двигателя. Самый минимум, чтобы запустить наш CDSL. Я думаю, вы увидите, как некоторые решения, которые мы приняли до сих пор, делают этот процесс намного проще, чем он мог бы быть.
Разбор
Во-первых, нам нужно преобразовать наш скрипт в список строк токенов. Это достаточно просто, просто извлеките их, используя великолепное и ужасное регулярное выражение, которое мы создали выше.
Токенизация
Когда у нас есть строки токенов, мы должны превратить их в токены (я знаю, это шокирует). Обратите внимание, что каждая строка токена на самом деле начинается с другого символа! Возможно, я планировал это заранее. Используя этот символ, мы превратим каждую строку токена в объект токена. Вот небольшой псевдокод.
function Tokenize(string tokenString) returns token { if tokenString[0] in listOfOperators: return CreateOperatorToken(tokenString) if tokenString[0] == " : return CreateStringToken(tokenString) if tokenString[0] == # : return CreateIntegerToken(tokenString) if tokenString[0] == ~ : return CreateFunctionToken(tokenString) if tokenString[0] in lowerCaseLetters : return CreateContextToken(tokenString) throw TokenizationException }
Вещи, конечно, не должны быть разбиты именно таким образом. Но каким бы ни был ваш метод, нам нужно создать объекты-токены с несколькими фрагментами данных.
Token { TokenType, IntegerValue, StringValue, NumberOfArguments }
NumberOfArguments
показывает структуру выполнения нашего CDSL. Думайте о каждом токене как о функции, если она не принимает никаких аргументов, то возвращает свое значение, если она принимает один или несколько аргументов, необходимо выполнить некоторое выполнение, прежде чем вы сможете получить указанное значение.
Сложите это
Мы перешли от строки к списку строк токенов, к списку токенов. Следующим шагом будет подготовка их к исполнению. Для этого нам нужно превратить наш список в стек с 0-м элементом наверху. Это даже не означает, что мы обязательно должны переносить токены в другую структуру данных. Это означает лишь то, что отныне мы будем как минимум использовать список таким образом. Вот неэффективный метод, просто для демонстрации.
function StackTokens(listOfTokens) returns Stack<Tokens> { let tempStack = new Stack<Tokens> let returnStack = new Stack<Tokens> for each token in listOfTokens: tempStack.Push(token) while tempStack is not empty: returnStack.Push(tempStack.Pop()) }
Теперь очевидно, почему важно придерживаться префиксной записи. Поместив нашу программу в стек с крайним левым маркером наверху, мы располагаем маркеры в идеальном порядке для рекурсивного выполнения.
Рекурсивное выполнение
Я действительно летаю в темноте на уровне объяснений, которые я должен давать. Слишком специфично, и ваши глаза остекленеют. Слишком мало, и может не получиться общаться с некоторыми членами аудитории. Но в любом случае мы достигли последнего шага; запуск скрипта и генерация вывода. Это можно сделать итеративно, но проще продемонстрировать (и закодировать) рекурсивно.
recursiveExec(tokenStack){ if tokenStack is empty: throw EmptyStackException let token = tokenStack.Pop() if token.NumberOfArguments is 0: return executeWithNoArguments(token) let args = list of type Token size token.NumberOfArguments for i in range 0 to token.NumberOfArguments: let arg = recursiveExec(tokenStack) args[i] = arg return executeWithArguments(token, args) }
Если это выглядит немного слишком просто, это потому, что я жульничаю. Функции executeWithNoArguments и executeWithArguments выполняют здесь тяжелую работу. Очевидно, что такие вещи, как интерполяция строк и вызовы функций, потребуют немного кода.
Это также очень простое исполнение. Мы не проверяем ошибки, кроме того, что выбрасываем исключение, когда не можем выполнить скрипт. Мы могли бы делать такие вещи, как убедиться, что стек пуст в конце выполнения, или предоставить трассировку стека в случае сбоя.
Я оставлю эти тонкости вашей собственной реализации.
Пример кода
Допустим, вы разрешаете пользователю отправлять себе оповещение, когда он использует определенный объем данных в своем плане. Им не разрешено устанавливать это оповещение на большее количество данных, чем их план MaxMb
. Если их план безлимитный, MaxMb
устанавливается равным нулю, и они могут установить предупреждение на любом уровне. Мы хотим, чтобы наш скрипт выводил сообщение об ошибке или пустую строку, если ввод верный. Наш контекст plan обозначается префиксом p внутри нашего CDSL. Наши настройки контекста обозначаются префиксом s.
?|=p.MaxMb#0<s.MaxMbWarnp.MaxMb"""You cannot set your warning level greater than your data limit of {1}Gb(s)"/p.MaxMb#1000
Это немного более читабельно с пробелами между нашими токенами:
? | = p.MaxMb #0 < s.MaxMbWarn p.MaxMb "" "You cannot set your warning level greater than your data limit of {1}Gb(s)" / p.MaxMb #1000
Если вы поместите их оба в Regex101 с регулярным выражением, которое мы создали для токенизации, вы увидите, что они создают одинаковые совпадения. Префиксная нотация может показаться немного странной, если вы к ней не привыкли, но ее легко усвоить.
Заключение
Я думаю, что изложил хороший пример того, почему CDSL может быть полезен для вашего проекта, а также отличное описание того, как его создать. Если вы думаете, что я что-то не так, дайте мне знать в комментариях. Это получилось довольно длинно, но я надеюсь, что вам было интересно. Подумайте о том, чтобы подписаться на мою среду, если вы хотите больше статей, подобных этой.