Design Pattern

Padrão de Projeto Interpreter em PHP com exemplo

Capa do artigo Padrão de Projeto Interpreter em PHP com exemplo
Leitura com foco

Entenda o contexto.
Leve para a prática.

Neste artigo
  1. Qual problema o Interpreter resolve?
  2. Exemplo do padrão Interpreter em PHP
  3. Testando o exemplo
  4. Quando usar o padrão Interpreter?
  5. Cuidados ao usar o Interpreter
  6. Interpreter e Composite: qual a diferença?

O Padrão de Projeto Interpreter representa a gramática de uma linguagem simples com classes e usa essas classes para interpretar sentenças dessa linguagem. Cada regra da gramática vira uma classe, e cada sentença vira uma árvore de objetos. Para saber o resultado, basta chamar interpret() no objeto do topo da árvore.

A linguagem aqui não é PHP nem SQL. É uma linguagem pequena, do seu domínio, como as regras de uma promoção ou de um filtro de busca.

Diagrama de classe UML do padrão de projeto Interpreter
Diagrama de classe UML do padrão de projeto Interpreter

Observe no diagrama os participantes do padrão:

  • AbstractExpression (Expression): a interface com o método interpret().
  • TerminalExpression (EqualsExpression e GreaterThanExpression): as folhas da árvore. Consultam o contexto e devolvem o resultado.
  • NonterminalExpression (AndExpression, OrExpression e NotExpression): combinam outras expressões.
  • Context (Context): os dados sobre os quais a sentença é avaliada.
  • Client: quem monta a árvore e chama interpret(). No exemplo, é o teste.

Qual problema o Interpreter resolve?

Pense em uma loja virtual em que a regra de frete grátis muda toda semana. Hoje ela é: pedido acima de R$ 200 para São Paulo, ou qualquer pedido de cliente VIP.

A solução direta é escrever a regra no código:

<?php

function hasFreeShipping(array $order): bool
{
    return ($order['total'] > 200 && $order['state'] === 'SP')
        || $order['customer'] === 'vip';
}

Funciona, mas cada mudança de regra exige alterar o código e publicar de novo. Com o Interpreter, a regra deixa de ser código e passa a ser um dado: uma árvore de objetos, que pode ser montada a partir de uma configuração, de um banco de dados ou de uma tela de cadastro.

A nossa linguagem de regras é pequena. Ela tem cinco construções:

Regra da linguagemSignificadoClasse
campo = valorO campo é igual ao valorEqualsExpression
campo > númeroO campo é maior que o númeroGreaterThanExpression
A AND BAs duas expressões são verdadeirasAndExpression
A OR BPelo menos uma é verdadeiraOrExpression
NOT AA expressão é falsaNotExpression
A gramática da linguagem de regras: cada linha virou uma classe.

Exemplo do padrão Interpreter em PHP

Vamos para o código. A interface Expression é o que toda expressão sabe fazer:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Interpreter;

interface Expression
{
    public function interpret(Context $context): bool;
}

O Context guarda os dados do pedido. Se a regra pedir um campo que não existe, ele avisa com uma exceção:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Interpreter;

use InvalidArgumentException;

final class Context
{
    /** @var array<string, string|int|float> */
    private array $values;

    /** @param array<string, string|int|float> $values */
    public function __construct(array $values)
    {
        $this->values = $values;
    }

    public function get(string $field): string|int|float
    {
        if (!array_key_exists($field, $this->values)) {
            throw new InvalidArgumentException(sprintf('Unknown field: %s', $field));
        }

        return $this->values[$field];
    }
}

Agora as expressões terminais. A EqualsExpression compara um campo com um valor:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Interpreter;

final class EqualsExpression implements Expression
{
    private string $field;
    private string $value;

    public function __construct(string $field, string $value)
    {
        $this->field = $field;
        $this->value = $value;
    }

    public function interpret(Context $context): bool
    {
        return $context->get($this->field) === $this->value;
    }
}

E a GreaterThanExpression verifica se um campo é maior que um número:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Interpreter;

final class GreaterThanExpression implements Expression
{
    private string $field;
    private float $value;

    public function __construct(string $field, float $value)
    {
        $this->field = $field;
        $this->value = $value;
    }

    public function interpret(Context $context): bool
    {
        return $context->get($this->field) > $this->value;
    }
}

As expressões não terminais recebem outras expressões e combinam os resultados. Os nomes têm o sufixo Expression porque and e or são palavras reservadas do PHP e não podem ser nome de classe:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Interpreter;

final class AndExpression implements Expression
{
    private Expression $left;
    private Expression $right;

    public function __construct(Expression $left, Expression $right)
    {
        $this->left = $left;
        $this->right = $right;
    }

    public function interpret(Context $context): bool
    {
        return $this->left->interpret($context) && $this->right->interpret($context);
    }
}
<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Interpreter;

final class OrExpression implements Expression
{
    private Expression $left;
    private Expression $right;

    public function __construct(Expression $left, Expression $right)
    {
        $this->left = $left;
        $this->right = $right;
    }

    public function interpret(Context $context): bool
    {
        return $this->left->interpret($context) || $this->right->interpret($context);
    }
}
<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Interpreter;

final class NotExpression implements Expression
{
    private Expression $expression;

    public function __construct(Expression $expression)
    {
        $this->expression = $expression;
    }

    public function interpret(Context $context): bool
    {
        return !$this->expression->interpret($context);
    }
}

Repare que uma AndExpression não sabe se os filhos são terminais ou outras combinações. Ela só chama interpret() em cada um. É isso que permite montar regras de qualquer tamanho.

Testando o exemplo

O terceiro teste monta a regra de frete grátis do começo do artigo e avalia três pedidos diferentes com ela:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Tests\Behavioral\Interpreter;

use Growthdev\DesignPatterns\Behavioral\Interpreter\AndExpression;
use Growthdev\DesignPatterns\Behavioral\Interpreter\Context;
use Growthdev\DesignPatterns\Behavioral\Interpreter\EqualsExpression;
use Growthdev\DesignPatterns\Behavioral\Interpreter\GreaterThanExpression;
use Growthdev\DesignPatterns\Behavioral\Interpreter\NotExpression;
use Growthdev\DesignPatterns\Behavioral\Interpreter\OrExpression;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;

final class InterpreterTest extends TestCase
{
    public function testExpectTerminalExpressionsToReadTheContext(): void
    {
        $context = new Context(['total' => 250.0, 'state' => 'SP']);

        $this->assertTrue((new GreaterThanExpression('total', 200.0))->interpret($context));
        $this->assertFalse((new GreaterThanExpression('total', 300.0))->interpret($context));
        $this->assertTrue((new EqualsExpression('state', 'SP'))->interpret($context));
        $this->assertFalse((new EqualsExpression('state', 'RJ'))->interpret($context));
    }

    public function testExpectNotToInvertAnExpression(): void
    {
        $context = new Context(['state' => 'SP']);
        $notFromRio = new NotExpression(new EqualsExpression('state', 'RJ'));

        $this->assertTrue($notFromRio->interpret($context));
    }

    public function testExpectFreeShippingRuleToBeInterpreted(): void
    {
        // (total > 200 AND state = SP) OR customer = vip
        $freeShipping = new OrExpression(
            new AndExpression(
                new GreaterThanExpression('total', 200.0),
                new EqualsExpression('state', 'SP')
            ),
            new EqualsExpression('customer', 'vip')
        );

        $bigOrderFromSaoPaulo = new Context(['total' => 250.0, 'state' => 'SP', 'customer' => 'regular']);
        $bigOrderFromRio = new Context(['total' => 250.0, 'state' => 'RJ', 'customer' => 'regular']);
        $smallOrderFromVip = new Context(['total' => 50.0, 'state' => 'RJ', 'customer' => 'vip']);

        $this->assertTrue($freeShipping->interpret($bigOrderFromSaoPaulo));
        $this->assertFalse($freeShipping->interpret($bigOrderFromRio));
        $this->assertTrue($freeShipping->interpret($smallOrderFromVip));
    }

    public function testExpectUnknownFieldToFail(): void
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('Unknown field: coupon');

        (new EqualsExpression('coupon', 'FRETE10'))->interpret(new Context(['total' => 250.0]));
    }
}

A regra é montada uma vez e reaproveitada com vários contextos. Para mudar a regra, basta montar outra árvore, sem tocar em nenhuma classe.

Quando usar o padrão Interpreter?

  • Quando o domínio tem uma linguagem simples que se repete: regras de promoção, filtros de busca, permissões, validações configuráveis.
  • Quando as regras precisam mudar sem alterar o código da aplicação.
  • Quando a gramática é pequena e o desempenho não é crítico.

Cuidados ao usar o Interpreter

  • Cada regra da gramática é uma classe. Em uma gramática grande, isso vira dezenas de classes difíceis de manter.
  • O padrão não cuida de transformar texto em árvore. No exemplo a árvore é montada à mão; ler uma regra escrita como texto exige um analisador (parser), que é outro trabalho.
  • Antes de escrever a sua linguagem, veja se uma biblioteca pronta resolve. O componente ExpressionLanguage do Symfony, por exemplo, avalia expressões escritas como texto e é indicado pela própria documentação como base para um motor de regras de negócio.
  • Nunca use eval() para avaliar regras vindas de fora. O manual do PHP desaconselha o uso porque ele permite executar qualquer código PHP.

Interpreter e Composite: qual a diferença?

A árvore de expressões do Interpreter tem a mesma estrutura do Composite: as expressões terminais são as folhas, e as não terminais são os grupos. A diferença está no objetivo de cada padrão:

PadrãoTipoFoco
InterpreterComportamentalDar significado a uma sentença: cada nó da árvore sabe se avaliar
CompositeEstruturalMontar a árvore e tratar folha e grupo do mesmo jeito
Diferença entre Interpreter e Composite.

Esse é mais um padrão comportamental do livro GoF. Com este artigo, os 23 padrões do livro têm um exemplo em PHP aqui no blog. A lista completa está no artigo Resumo dos Padrões de Projetos.

O código completo deste exemplo, com os testes, está no meu GitHub, na pasta src/Behavioral/Interpreter do repositório growthdev-repo/design-patterns.

Veja também: o resumo dos 23 padrões de projeto GoF e os outros padrões comportamentais já publicados: Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor.

Confiança Sempre!!!

Fontes:

  • [1] GAMMA, Erich et al. Padrões de Projeto: Soluções reutilizáveis de software orientado a objetos.
  • [2] FREEMAN, Eric; ROBSON, Elisabeth; BATES, Bert; SIERRA, Kathy. Head First Design Patterns.
  • [3] Symfony Docs. Expression Language.
  • [4] PHP Manual. eval.
Sobre quem escreve

Walmir Silva

Olá! Sou Walmir, engenheiro de software com MBA em Engenharia de Software e o cérebro por trás do GrowthCode e autor do livro "Além do Código".

Se você acha que programação é apenas sobre escrever código, prepare-se para expandir seus horizontes. Aqui, nós vamos além do código e exploramos as interseções fascinantes entre tecnologia, negócios, artes e filosofia.

Você está em busca de crescimento na carreira? Quer se destacar em um mercado competitivo? Almeja uma vida mais rica em conhecimento e realização? Então você chegou ao lugar certo. No GrowthCode, oferecemos insights profundos, estratégias comprovadas e um toque de sabedoria filosófica para catalisar seu crescimento pessoal e profissional.

Ver todos os artigos →

Continue a conversa

Compartilhe sua perspectiva

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Encontre sua próxima resposta

O que você quer aprender?

Preferências de cookies

Os cookies opcionais começam desligados. Você pode mudar a escolha a qualquer momento por esta tela.

NecessáriosSempre ativos

Guardam a sua escolha de cookies e mantêm o site funcionando. Não são usados para publicidade.

Contam visitas e mostram como o site é usado: Google Analytics e Microsoft Clarity, carregados pelo Google Tag Manager. Como esse contêiner também carrega tags de publicidade, as estatísticas só são ativadas junto com Publicidade e marketing.

Exibem anúncios do Google AdSense e medem campanhas e e-mails: pixels da Meta e do TikTok e o Mautic.

Permite que o Google escolha anúncios com base nos seus interesses. Desligado, os anúncios aparecem sem personalização. Depende de Publicidade e marketing.

Também é possível bloquear cookies nas configurações do navegador. Detalhes na Política de privacidade.