Как стать автором
Обновить

DesignPatterns, шаблон Interpreter

Время на прочтение5 мин
Количество просмотров18K
Компиляторы и интерпретаторы языков, как правило, пишутся на других языках(по крайней мере так было сначала). Например PhP был написан на языке С. Как вы догадались на языке PhP можно определить, спроектировать и написать свой компилятор своего языка. Конечно язык который бы создадим будет медленно работать и будет ограничен, тем не менее он может быть очень полезным.

И так создадим проблему которую надо решить

Как правило при разработке приложения мы предоставляем пользователю некий функционал(api), при разработке интерфейса приходится искать компромисс между функциональностью и простотой использования и естественно что чем больше функционал тем более запутан интерфейс. Вероятно можно упростить интерфейс без «обрезания» функционала приложения. Предложим нашим пользователям предметно-ориентированный язык программирования(который обычно называют DSL, или Domain Specific Language), вы можете реально расширить функционал приложения.
Конечно можно позволить пользователю создавать свои сценарии в нашей системе и без промежуточного языка программирования:

$input = $_REQUEST['input'];
//Содержит «print file_get_contents('/etc/passwd');
eval($input);


Очевидно что такой способ нельзя назвать нормальным. Но если вам не очевидно, то на это есть две причины: безопасность и сложность.

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

Пример вопроса:
Сколько планет в нашей солнечной системе?
Мы можем принимать ответы «восемь», «8», в качестве правильных ответов. Конечно это можно было бы решить с помощью всеми любимых регулярных выражений, ^8|восемь$.
Но к сожалению, не все разработчики знают их. Поэтому давайте реализуем более дружественный интерфейс:

$input equals «8» or $input equals «восемь»

Требуется создать язык который поддерживает переменные, оператор equals и булевую логику(or, and). Так как мы все любим все называть давайте назовем наш язык, GoLogic.
Наш язык должен четко расширятся, поскольку мы предвидим множество запросов на более сложные функции. Оставим в стороне вопрос анализа входных данных и перейдем к архитектуре механизма подключения этих элементов для генерации ответа.

Реализация

Даже в нашем небольшом языке необходимо отслеживать множество элементов это и переменные, и строковые литералы, и булево И и булево ИЛИ и проверка равенства.
Разобьем их на классы:
image

Нарисуем небольшую диаграмму:
image

Немного кода:
abstract class Expression {

    private static $keycount = 0;
    private $key;

    abstract function interpret(InterpreterContext $context);

    function getKey() {
        if (!isset($this->key)) {
            self::$keycount++;
            $this->key = self::$keycount;
        }
        return $this->key;
    }

}

class LitralExpr extends Expression {

    private $value;

    function __construct($value) {
        $this->value = $value;
    }

    public function interpret(InterpreterContext $context) {
        $context->replace($this, $this->value);
    }

}

class InterpreterContext {

    private $exprstore = array();

    function replace(Expression $exp, $value) {
        $this->exprstore[$exp->getKey()] = $value;
    }

    function lookup(Expression $exp) {
        return $this->exprstore[$exp->getKey()];
    }

}


InterpreterContext предоставляет только интерфейс для ассоциативного массива $exprstore который мы используем для хранения данных. Методу replace() передаем ключ и значение, так же реализован метод lookup() для извлечения данных.
В классе Expression определен абстрактный метод interpre() и конкретный метод getKey(); оперирующий статическим значением счетчика, оно и возвращается в качестве дескриптора выражения, это метод используем для индексирования данных.
В классе LiteralExpr определен конструкто, которому передается аргумент-значение. Методу interept() нужно передать объект типа InterpreterContex в нем просто вызываем метод replace()
Теперь определим оставшийся класс VariableExpr
class VariableExpr extends Expression {

    private $name;
    private $val;

    public function __construct($name, $val = null) {
        $this->name = $name;
        $this->val = $val;
    }

    public function interpret(InterpreterContext $context) {
        if (!is_null($this->val)) {
            $context->replace($this, $this->val);
            $this->val = null;
        }
    }

    function setValue($val) {
        $this->val = $val;
    }

    function getKey() {
        return $this->name;
    }

}


Конструктора передается два аргумента (имя и значение), которые сохраняются в свойствах объекта. В классе реализован метод setValue(), что бы клиентский код мог изменить значение в любой момент.
Метод interept() проверят, имеет ли свойство $val нулевое значение, если есть значение то его значение сохранится в объекте InterpreterContext. Затем мы устанавливаем переменной $val значение null это делается для того что бы повторный вызов метода не испортил значение переменной с тем же именем, сохраненной в объекте InterpreterContext другим экземпляром объекта VariableExpr.
Теперь добавим немного логики.

abstract class OperatorExpr extends Expression {

    protected $l_op;
    protected $r_op;

    function __construct(Expression $l_op, Expression $r_op) {
        $this->l_op = $l_op;
        $this->r_op = $r_op;
    }

    function interpret(InterpreterContext $context) {
        $this->l_op->interpret($context);
        $this->r_op->interpret($context);
        $result_l = $context->lookup($this->l_op);
        $result_r = $context->lookup($this->r_op);
        $this->doInterpret($context, $result_l, $result_r);
    }

    protected abstract function doInterpret(InterpreterContext $context, $result_l, $result_r);
}


метод doInterpret() представляет собой экземпляр шаблона Template Method. В этом шаблоне в родительском классе и определяется и вызывается абстрактный метод, реализация которого оставляется дочерним классам. Это может упростить разработку конкретных классов, поскольку совместно используемыми функциями управляет супер класс, оставляя дочерним задачу сконцентрироваться на четких и понятных целях.

class EqualsExpr extends OperatorExpr {

    protected function doInterpret(InterpreterContext $context, $result_l, $result_r) {
        $context->replace($this, $result_l == $result_r);
    }

}

class BoolOrExpr extends OperatorExpr {

    protected function doInterpret(InterpreterContext $context, $result_l, $result_r) {
        $context->replace($this, $result_l || $result_r);
    }

}

class BoolAndExpr extends OperatorExpr {

    protected function doInterpret(InterpreterContext $context, $result_l, $result_r) {
        $context->replace($this, $result_l && $result_r);
    }

}


Теперь имея подготовленные классы и маленькую систему, мы готовы выполнить небольшой код. Еще помните его?
$input equals «8» or $input equals «восемь»

$context = new InterpreterContext();
$input = new VariableExpr('input');
$stats = new BoolOrExpr(new EqualsExpr($input, new LitralExpr('Восемь')), new EqualsExpr($input, new LitralExpr('8')));
foreach (array("Восемь", "8", "666") as $val) {
    $input->setValue($val);
    print "$val:\n";
    $stats->interpret($context);
    if($context->lookup($stats)) {
        print 'равны\n\n';
    } else {
        print "не равны\n\n";
    }
}


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

image
Проблемы шаблона Interpreter

Как только вы подготовите основные классы для реализации шаблона Interpreter, расширить его будет легко. Цена, которую за это приходится платить, — только количество классов, которые нужно создать. Поэтому данный шаблон более применим для относительно не больших языков. А если вам нужен полноценный язык программирования, то лучше искать для этого инструменты от сторонних фирм.

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

Источники:
Шаблоны «Банды Четырех»
Теги:
Хабы:
+6
Комментарии15

Публикации

Истории

Работа

PHP программист
159 вакансий

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн