Интерпретатор (Interpreter) — шаблон проектирования

30 января 2020
Шаблон предлагает реализовать собственный язык запросов и его интерпретатор. Это позволяет писать выражения, формулы, фильтры, шаблоны, анализировать их, переводить в программные действия и выполнять обработки. 

ПРИМЕР ЗАДАЧИ: является ли числом, обязательность заполнения поля, проверка величины значения — больше или меньше).

Код реализации паттерна на языке PHP. Скачать исходники (ZIP, 4 Кб)

В шаблоне используются следующие понятия:
  • Терминальное выражение (Terminal expression) — неделимые части грамматики будущего языка. К терминальным выражениям относятся цифры, слова и любые другие элементы, которые будут считываться интерпретатором без необходимости разделения/декомпозиции элемента на составные части. Под каждый тип терминального выражения создаётся свой класс с методом interpret($context) для интерпретации выражения;
  • Нетерминальное выражение (Nonterminal expression) — составные части грамматики будущего языка. К нетерминальным выражениям относятся конструкции с участием знаков математических операций, операторы и другие элементы, которые интерпретатор может разбить на простые или терминальные выражения;
  • Контекст — глобальный буфер, хранящий промежуточные вычисления, значения переменных и констант. Используется при интерпретации выражений.

В примере задачи, правило, написанное на нашем языке, будет выглядеть так:
$rule = 'Это число, обязательное, значение > 10, значение <= 50';

test.php

Файл содержит тестовый скрипт, в котором задаётся массив потенциальных значений текстового поля, затем указывается правило для проверки этих значений. Далее происходит обход массива и проверка значений на соответствие правилу с помощью метода Field::checkValue($value, $rule) класса полей Field

Код скрипта:
<?
//Подключение класса поля
require_once(__DIR__.'/field.class.php');

//Массив значений, которые будем проверять
$arValues = array(
    5,
    15,
    '',
    'Привет',
    50
);

//Правило проверки значения
$rule = '"Это число, обязательное, значение > 10, значение <= 50';

//Обходим каждый элемент массива и проверяем значение на соответствие правилу
foreach ($arValues as $value) {
    $isValid = Field::checkValue($value, $rule);
    if ($isValid == true) {
        echo 'Значение «'.$value.'» корректное<br>';
    } else {
        echo 'Значение «'.$value.'» некорректное<br>';
    }
}

?>

field.class.php

Содержит класс Field для работы с текстовыми полями. В рамках демонстрации класс содержит один статический метод checkValue($value, $rule), используемый для проверки значения $value  на соответствие правилу $rule. Так как правила состоят из выражений, соединённые запятыми, то метод разбивает правило на выражения, используя в качестве разделителя символ запятой , и затем для каждого выражения подбирает класс-выражение.
<?
/**
 * Класс текстового поля
 */
class Field
{
    /**
     * Проверяет корректность значения по установленному правилу
     * 
     * @param string $value - значение текстового поля
     * @param string $rule - правило проверки поля
     * @return - вернёт 'true', если значение корректное и соотвествует правилу, в противном случае вернёт 'false'
     */
    public static function checkValue($value, $rule = '') 
    {
        //Подключение классов интерпретатора
        require_once(__DIR__.'/interpreter.classes.php');

        if ($rule) {
            //Создаём контекст и помещаем в него значение
            $context = new Context();
            $context->setVariable('Значение', $value);

            //Убираем из правила ненужные пробелы и приводим строку к нижнему регистру
            $rule = mb_strtolower(trim($rule), 'utf-8');

            /*
            Разбиваем правило на части и для каждой части подбираем соответствующий ей объект выражения.
            Затем этот объект добавляем в массив выражений для последующего составления конечного (общего) выражения
            */
            $arExpressions = array();
            $arStrExpressions = explode(',', $rule);
            foreach ($arStrExpressions as $strExpression) {
                //Убираем в подправиле повторяющиеся пробелы
                $strExpression = trim(preg_replace('/\s+/', ' ', $strExpression));

                //Выражение "Это число"
                if (mb_strpos($strExpression, 'это число', 0, 'utf-8') !== false) {
                    $arExpressions[] = new IsNumberExpression(
                        new VariableExpression('Значение')
                    );
                }
                //Выражение "Обязательное" 
                else if (mb_strpos($strExpression, 'обязательное', 0, 'utf-8') !== false) {
                    $arExpressions[] = new NotEmptyExpression(
                        new VariableExpression('Значение')
                    );
                } 
                //Выражение для проверки значения на меньше или равно <=
                else if (mb_strpos($strExpression, '<=', 0, 'utf-8') !== false) {
                    $partsExpression = explode(' ', $strExpression);
                    $arExpressions[] = new CompareSmallerOrEqualExpression(
                        new VariableExpression('Значение'),
                        new NumberExpression($partsExpression[2])
                    );
                } 
                //Выражение для проверки значения на меньше <
                else if (mb_strpos($strExpression, '<', 0, 'utf-8') !== false) {
                    $partsExpression = explode(' ', $strExpression);
                    $arExpressions[] = new CompareSmallerExpression(
                        new VariableExpression('Значение'),
                        new NumberExpression($partsExpression[2])
                    );
                } 
                //Выражение для проверки значения на больше или равно >=
                else if (mb_strpos($strExpression, '>=', 0, 'utf-8') !== false) {
                    $partsExpression = explode(' ', $strExpression);
                    $arExpressions[] = new CompareBiggerOrEqualExpression(
                        new VariableExpression('Значение'),
                        new NumberExpression($partsExpression[2])
                    );
                } 
                //Выражение для проверки значения на больше >
                else if (mb_strpos($strExpression, '>', 0, 'utf-8') !== false) {
                    $partsExpression = explode(' ', $strExpression);
                    $arExpressions[] = new CompareBiggerExpression(
                        new VariableExpression('Значение'),
                        new NumberExpression($partsExpression[2])
                    );
                }
            }

            //Итоговое выражение
            $finalExpression = new MultiplicationExpression($arExpressions);

            return (bool)$finalExpression->interpret($context);
        } else {
            return true;
        }
    }
}

?>

interpreter.classes.php

Содержит класс контекста (окружение и буфер) и классы выражений:
  • Context — контекст (окружение и буфер, хранящий промежуточные вычисления, значения переменных и констант). Используется при интерпретации выражений (вычисления значений выражений);
  • Expression — абстрактный класс выражения. В классе объявлен абстрактный метод interpret($context), выполняющий интерпретацию (вычисление) выражения и возвращает подсчитанное значение. В дочерних классах этот метод определён, так как у каждого выражения своя логика вычисления результата выражения;
  • StringExpression — выражение-строка. Метод interpret($context) возвращает исходную строку;
  • NumberExpression — выражение-целочисленное число. Метод interpret($context) возвращает исходное число;
  • VariableExpression — выражение-переменная. Метод interpret($context) возвращает значение переменной;
  • IsNumberExpression — выражение «Это число», проверяет значение выражения на число. Метод interpret($context) вернёт 1 (единицу), если значение выражения является числом, в противном случае вернёт 0 (ноль);
  • NotEmptyExpression — выражение «Не пусто», проверяет значение на непустоту. Метод interpret($context) вернёт 1 (единицу), если значение выражения непустое, в противном случае вернёт 0 (ноль);
  • CompareBiggerExpression — выражение-сравнение «Больше». Cравнивает левое и правое выражение с помощью знака «Больше» >. Метод interpret($context) вернёт 1 (единицу), если значение левого выражения больше правого, в противном случае вернёт 0 (ноль);
  • CompareBiggerOrEqualExpression — выражение-сравнение «Больше или равно». Cравнивает левое и правое выражение с помощью знака «Больше или равно» >=. Метод interpret($context) вернёт 1 (единицу), если значение левого выражения больше или равно правому, в противном случае вернёт 0 (ноль);
  • CompareSmallerExpression — выражение-сравнение «Меньше». Cравнивает левое и правое выражение с помощью знака «Меньше» <. Метод interpret($context) вернёт 1 (единицу), если значение левого выражения меньше правого, в противном случае вернёт 0 (ноль);
  • CompareSmallerOrEqualExpression — выражение-сравнение «Меньше или равно». Cравнивает левое и правое выражение с помощью знака «Меньше или равно» <=. Метод interpret($context) вернёт 1 (единицу), если значение левого выражения меньше или равно правому, в противном случае вернёт 0 (ноль);
  • MultiplicationExpression — выражение-умножение. Метод interpret($context) обходит выражения, получает их значения и перемножает между собой, а в конце возвращает результат всех перемножений.
<?
/**
 * Класс текстового поля
 */
class Field
{
    /**
     * Проверяет корректность значения по установленному правилу
     * 
     * @param string $value - значение текстового поля
     * @param string $rule - правило проверки поля
     * @return - вернёт 'true', если значение корректное и соотвествует правилу, в противном случае вернёт 'false'
     */
    public static function checkValue($value, $rule = '') 
    {
        //Подключение классов интерпретатора
        require_once(__DIR__.'/interpreter.classes.php');

        if ($rule) {
            //Создаём контекст и помещаем в него значение
            $context = new Context();
            $context->setVariable('Значение', $value);

            //Убираем из правила ненужные пробелы и приводим строку к нижнему регистру
            $rule = mb_strtolower(trim($rule), 'utf-8');

            /*
            Разбиваем правило на части и для каждой части подбираем соответствующий ей объект выражения.
            Затем этот объект добавляем в массив выражений для последующего составления конечного (общего) выражения
            */
            $arExpressions = array();
            $arStrExpressions = explode(',', $rule);
            foreach ($arStrExpressions as $strExpression) {
                //Убираем в подправиле повторяющиеся пробелы
                $strExpression = trim(preg_replace('/\s+/', ' ', $strExpression));

                //Выражение "Это число"
                if (mb_strpos($strExpression, 'это число', 0, 'utf-8') !== false) {
                    $arExpressions[] = new IsNumberExpression(
                        new VariableExpression('Значение')
                    );
                }
                //Выражение "Обязательное" 
                else if (mb_strpos($strExpression, 'обязательное', 0, 'utf-8') !== false) {
                    $arExpressions[] = new NotEmptyExpression(
                        new VariableExpression('Значение')
                    );
                } 
                //Выражение для проверки значения на меньше или равно <=
                else if (mb_strpos($strExpression, '<=', 0, 'utf-8') !== false) {
                    $partsExpression = explode(' ', $strExpression);
                    $arExpressions[] = new CompareSmallerOrEqualExpression(
                        new VariableExpression('Значение'),
                        new NumberExpression($partsExpression[2])
                    );
                } 
                //Выражение для проверки значения на меньше <
                else if (mb_strpos($strExpression, '<', 0, 'utf-8') !== false) {
                    $partsExpression = explode(' ', $strExpression);
                    $arExpressions[] = new CompareSmallerExpression(
                        new VariableExpression('Значение'),
                        new NumberExpression($partsExpression[2])
                    );
                } 
                //Выражение для проверки значения на больше или равно >=
                else if (mb_strpos($strExpression, '>=', 0, 'utf-8') !== false) {
                    $partsExpression = explode(' ', $strExpression);
                    $arExpressions[] = new CompareBiggerOrEqualExpression(
                        new VariableExpression('Значение'),
                        new NumberExpression($partsExpression[2])
                    );
                } 
                //Выражение для проверки значения на больше >
                else if (mb_strpos($strExpression, '>', 0, 'utf-8') !== false) {
                    $partsExpression = explode(' ', $strExpression);
                    $arExpressions[] = new CompareBiggerExpression(
                        new VariableExpression('Значение'),
                        new NumberExpression($partsExpression[2])
                    );
                }
            }

            //Итоговое выражение
            $finalExpression = new MultiplicationExpression($arExpressions);

            return (bool)$finalExpression->interpret($context);
        } else {
            return true;
        }
    }
}

?>

В нашем правиле, записанном в переменной $rule, к терминальным выражениям относятся фразы: 
  1. Число — соответствует классу IsNumberExpression;
  2. обязательное — соответствует классу NotEmptyExpression;

А к нетерминальным выражениям относятся фразы: 
  1. значение > 10
    Данное выражения при интерпретации будет разбито на три терминальных выражения:
    • значение — соответствует классу выражения-переменной VariableExpression;
    • > — соответствует классу CompareBiggerExpression;
    • 10 — соответствует классу NumberExpression.
  2. значение <= 50
    Данное выражения при интерпретации будет разбито на три терминальных выражения:
    • значение — соответствует классу выражения-переменной VariableExpression;
    • <= — соответствует классу CompareSmallerOrEqualExpression;
    • 50 — соответствует классу NumberExpression.

Результат работы скрипта test.php
Значение «5» некорректное
Значение «15» корректное
Значение «» некорректное
Значение «Привет» некорректное
Значение «50» корректное