Friend spotlight!
Whimsical Animations course
Friend spotlight!
NEW Whimsical Animations course
Friend spotlight! NEW Whimsical Animations course
huge discount only this week
Friend spotlight! Want to make your project stand out? NEW Whimsical Animations course huge discount only this week

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.