Visitor (padrão de desenho)/Expressões Aritméticas Simples

From Wiki**3

< Visitor (padrão de desenho)

O exemplo seguinte mostra um exemplo de uso do padrão Visitor.

O ponto de partida é uma hierarquia de expressões inteiras: neste exemplo, usam-se as funções aritméticas simples e um valor literal.

Para motivar o uso do padrão, considere-se a class ADD (soma binária), definida como se segue (BinaryFunction é uma superclasse abstracta comum a todas as operações binárias, que contém dois atributos do tipo Content (a superclasse de todas as expressões; neste primeiro exemplo, assume-se que declara o método value(), que retorna o valor calculado).

Nesta classe, o método value() calcula o valor da expressão recursivamente, primeiro, pelo lado esquerdo (left()) e, depois, pelo lado direito (right()).

public class ADD extends BinaryFunction {
  public int value() {
    return left().value() + right().value();
  }
}

Esta implementação é suficiente para a avaliação da expressão. No entanto, que seria necessário alterar para conseguir utilizar a expressão ADD para outro fim? Por exemplo, para a traduzir para C? ou para outro fim?

As respostas àquelas perguntas poderiam ser todas "acrescentar um método para esse fim". No entanto, isso vem alterar a definição original da classe e novas evoluções são igualmente acompanhadas de impacto negativo e generalizado sobre este tipo de classes (as alterações teriam, potencialmente, que ser realizadas também em classes semelhantes).

Utilizando o padrão Visitor, é possível remover as dependências de implementações específicas das classes de uma hierarquia de dados (esta hierarquia é, frequentemente, um Composite criado por uma fábrica).

Classes das Expressões (Composite)

As classes das expressões estão organizadas segundo o padrão Composite e assentam sobre uma interface de topo. Escolheu-se uma interface e não uma classe abstracta por simplicidade e porque não há consequências relativamente a repetição de código.

public interface Content {
  int accept(Visitor v);
}

Note-se que a interface, embora seja um tipo abstracto (nunca sendo realmente processada pelo "visitor"), tem de declarar uma interface comum a todas as expressões. Dada a simplicidade do cenário, apenas é necessário o método accept. Note-se que não é especificado qual o "visitor" específico.

Abaixo desta interface existem duas classes que a implementam: a dos literais (Literal) e a das funções binárias (BinaryFunction). Assume-se que os literais são os únicos que guardam valores inteiros e que todas as outras expressões se definem recursivamente (Composite).

Note-se a implementação de accept.

public class Literal implements Content {
  private int _value;
  public Literal(int value) { _value = value; }
  public int accept(Visitor v) { return v.visitLiteral(this); }
  public int value() { return _value; }
}

A classe de base para as funções binárias limita-se a definir os atributos e métodos relacionados. Sendo abstracta, não necessita definir accept (será definido por todas as suas subclasses concretas).

public abstract class BinaryFunction implements Content {
  private Content _left;
  private Content _right;
  public BinaryFunction(Content left, Content right) { _left = left; _right = right; }
  public Content left() { return _left; }
  public Content right() { return _right; }
}

Finalmente, as quatro classes das expressões aritméticas simples (soma, subtracção, multiplicação, divisão), são definidas. Note-se a extrema simplicidade destas classes: ao ser utilizado um "visitor", o comportamento delas migrou para as classes do padrão Visitor, deixando apenas a definição da inicialização e o método accept.

public class ADD extends BinaryFunction {
  public ADD(Content left, Content right) { super(left, right); }
  public int accept(Visitor v) { return v.visitADD(this); }
}
public class SUB extends BinaryFunction {
  public SUB(Content left, Content right) { super(left, right); }
  public int accept(Visitor v) { return v.visitSUB(this); }
}
public class MUL extends BinaryFunction {
  public MUL(Content left, Content right) { super(left, right); }
  public int accept(Visitor v) { return v.visitMUL(this); }
}
public class DIV extends BinaryFunction {
  public DIV(Content left, Content right) { super(left, right); }
  public int accept(Visitor v) { return v.visitDIV(this); }
}

Classes de Visita ("visitors") (Visitor)

A interface Visitor define os métodos que devem ser implementados por todas as implementações. Implementações particulares podem definir outros métodos ou mesmo atributos. Por simplicidade, declara-se o retorno como um inteiro (isto permite que sejam obtidos valores recursivamente quando for apropriado).

public interface Visitor {
  int visitADD(ADD c);
  int visitSUB(SUB c);
  int visitMUL(MUL c);
  int visitDIV(DIV c);
  int visitLiteral(Literal c);
}

No exemplo que aqui se apresenta, define-se, primeiro, um "visitor" para avaliação de expressões. A sua passagem a um método accept é como a chamada, no modelo original, ao método value sobre a classe original correspondente.

public class Evaluator implements Visitor {
  public int visitADD(ADD c) {
    return c.left().accept(this) + c.right().accept(this);
  } 
  public int visitSUB(SUB c) {
    return c.left().accept(this) - c.right().accept(this);
  } 
  public int visitMUL(MUL c) {
    return c.left().accept(this) * c.right().accept(this);
  } 
  public int visitDIV(DIV c) {
    return c.left().accept(this) / c.right().accept(this);
  } 
  public int visitLiteral(Literal c) {
    return c.value();
  } 
}

Como se pode ver, a recursão termina nos literais. Compare-se ainda esta implementação com a classe ADD mostrada acima.

De seguida, para demonstrar como a funcionalidade pode ser muito diferente de classe de visita para classe de visita, mostra-se a classe CWriter que é capaz de produzir o código C para avaliar uma expressão.

public class CWriter implements Visitor {
  String _cProgram = "";
  public String toString() {
    String program = "#include <stdio.h>\nint main() {\n\tprintf(\"%d\\n\", ";
    program += _cProgram;
    program += ");\n\treturn 0;\n}\n";
    return program;
  }
  public int visitADD(ADD c) {
    _cProgram += "(";
    c.left().accept(this);
    _cProgram += "+";
    c.right().accept(this);
    _cProgram += ")";
    return 0;
  }
  public int visitSUB(SUB c) {
    _cProgram += "(";
    c.left().accept(this);
    _cProgram += "-";
    c.right().accept(this);
    _cProgram += ")";
    return 0;
  }
  public int visitMUL(MUL c) {
    _cProgram += "(";
    c.left().accept(this);
    _cProgram += "*";
    c.right().accept(this);
    _cProgram += ")";
    return 0;
  }
  public int visitDIV(DIV c) {
    _cProgram += "(";
    c.left().accept(this);
    _cProgram += "/";
    c.right().accept(this);
    _cProgram += ")";
    return 0;
  }
  public int visitLiteral(Literal c) {
    _cProgram += Integer.toString(c.value());
    return 0;
  }
}

A ideia é produzir uma cadeia de caracteres com o programa C completo. De seguida, o método toString (o processo poderia ser outro) apresenta o programa equivalente. Utiliza-se este método por simplicidade: numa outra situação, o seu nome poderia ser outro.

Aplicação

Finalmente, a aplicação começa por criar a composição (simulando uma fábrica) e envia instâncias das duas classes de visita, imprimindo os respectivos resultados.

public class App {
  public static void main(String[] args) {
    // 1 + 2 * 3
    Content c = new ADD(new Literal(1), new MUL(new Literal(2), new Literal(3)));
    //
    int value = c.accept(new Evaluator());
    System.out.println("1 + 2 * 3 = " + value);
    //
    Visitor cWriter = new CWriter();
    c.accept(cWriter);
    System.out.println("==========================================");
    System.out.println(cWriter);
  }
}

Resultados

O resultado é obtido através da execução do método main (assume-se a compilação prévia de todas as classes):

java App

A saída é a seguinte:

1 + 2 * 3 = 7
==========================================
#include <stdio.h>
int main() {
        printf("%d\n", (1+(2*3)));
        return 0;
}