REDUÇÃO de Inverno

Visitor e Double Dispatch

Vamos dar uma olhada na seguinte hierarquia de classe para formas geométricas (cuidado com o pseudocódigo):

interface Graphic is
    method draw()

class Shape implements Graphic is
    field id
    method draw()
    // ...

class Dot extends Shape is
    field x, y
    method draw()
    // ...

class Circle extends Dot is
    field radius
    method draw()
    // ...

class Rectangle extends Shape is
    field width, height
    method draw()
    // ...

class CompoundGraphic implements Graphic is
    field children: array of Graphic
    method draw()
    // ...

O código funciona bem e a aplicação está em produção. Mas um dia você decide criar uma funcionalidade de exportação. O código da exportação seria alienígena se colocado nessas classes. Então ao invés de acionar a exportação para todas as classes dessa hierarquia você decidiu criar uma nova classe, externa à hierarquia e colocar toda a lógica de exportação lá dentro. A classe obteria métodos para a exportação do estado público de cada objeto em strings XML:

class Exporter is
    method export(s: Shape) is
        print("Exportando forma")
    method export(d: Dot)
        print("Exportando Ponto")
    method export(c: Circle)
        print("Exportando círculo")
    method export(r: Rectangle)
        print("Exportando retângulo")
    method export(cs: CompoundGraphic)
        print("Exportando composto")

O código parece bom, mas vamos testá-lo:

class App() is
    method export(shape: Shape) is
        Exporter exporter = new Exporter()
        exporter.export(shape);

app.export(new Circle());
// Infelizmente, isso irá imprimir "Exportando forma".

Espera aí! Por quê?!

Pensando como um compilador

Nota: a seguinte informação é verdadeira para a maioria das linguagens de programação modernas orientadas aos objetos (Java, C#, PHP, e outras).

Vinculação tardia/dinâmica

Finja que você é um compilador. Você tem que decidir como compilar o seguinte código:

method drawShape(shape: Shape) is
    shape.draw();

Vejamos... o método draw definido na classe Shape. Espera um segundo, mas também há quatro subclasses que sobrescrevem esse método. Podemos decidir com segurança qual das implementações chamar aqui? Parece que não. A única maneira de saber com certeza é rodar o programa e checar a classe de um objeto passado para o método. A única coisa que sabemos de certeza é que o objeto terá a implementação do método draw.

Então o código máquina resultante irá checar a classe pelo parâmetro s e pegar a implementação draw da classe apropriada.

Esse tipo de checagem dinâmica é chamada de vinculação tardia (ou dinâmica):

  • Tardia, porque nós ligamos o objeto e sua implementação após a compilação no tempo de execução.
  • Dinâmica, porque cada novo objeto pode precisar estar ligado à uma implementação diferente.

Vinculação antecipada/estática

Agora, vamos “compilar” o seguinte código:

method exportShape(shape: Shape) is
    Exporter exporter = new Exporter()
    exporter.export(shape);

Tudo fica claro com a segunda linha: a classe Exporter não tem um construtor, então nós apenas instanciamos um objeto. E a chamada para o export? A Exporter tem cinco métodos com o mesmo nome que diferem por tipos de parâmetro. Qual delas chamar? Parece que nós vamos precisar de uma vinculação dinâmica aqui também.

Mas há outro problema. E se houver uma classe de “forma” que não tem o método export apropriado na classe Exporter? Por exemplo, um objeto Ellipse. O compilador não pode garantir que o método de sobrecarregamento adequado exista em contraste com os métodos sobrescritos. Uma situação ambígua aparece que o compilador não pode permitir.

Portanto, desenvolvedores de compiladores usam um caminho seguro e usam a vinculação antecipada (ou estática) para métodos sobrecarregados:

  • Antecipada porque acontece durante o tempo de compilação, antes do programa ser rodado.
  • Estática porque não pode ser alterada no tempo de execução.

Vamos voltar ao nosso exemplo. Nós temos certeza que o argumento que está vindo será da hierarquia Shape: seja da classe Shape ou uma de suas subclasses. Nós também sabemos que a classe Exporter tem uma implementação básica da exportação que suporta a classe Shape: export(s: Shape).

Essa é a única implementação que pode ser ligada de forma segura a um código sem tornar as coisas ambíguas. É por isso que mesmo que passamos um objeto Rectangle em exportShape, o exportador ainda vai chamar um método export(s: Shape).

Double Dispatch (Despacho Duplo)

O double dispatch é um truque que permite usar a vinculação dinâmica junto com método sobrecarregados. Veja como é feito:

class Visitor is
    method visit(s: Shape) is
        print("Forma visitada")
    method visit(d: Dot)
        print("Ponto visitado")

interface Graphic is
    method accept(v: Visitor)

class Shape implements Graphic is
    method accept(v: Visitor)
        // O compilador sabe com certeza que `this` é uma `Shape`.
        // O que significa que o `visit(s: Shape)` pode ser chamado com segurança.
        v.visit(this)

class Dot extends Shape is
    method accept(v: Visitor)
        // O compilador sabe que `this` é um `Dot`.
        // O que significa que o `visit(s: Dot)` pode ser chamado com segurança.
        v.visit(this)


Visitor v = new Visitor();
Graphic g = new Dot();

// O método `accept` foi sobrescrito, não sobrecarregado. O compilador
// vinculou ele dinamicamente. Portanto o `accept` será executado em uma
// classe que corresponda a um método de chamada de objeto (no nosso caso,
// a classe `Dot`).
g.accept(v);

// Saída: "Ponto visitado"

Posfácio

Mesmo que o padrão Visitor seja construído no princípio do double dispatch (despacho duplo), esse não é seu propósito principal. O Visitor permite que você adicione operações “externas” para toda uma hierarquia de classe sem mudar o código existente dessas classes.