Visitor e Double Dispatch.
Vamos dar uma olhada na seguinte hierarquia de classes de formas geométricas (cuidado, é 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ê decidiu criar um recurso de exportação. O código de exportação ficaria deslocado se colocado nessas classes. Então, em vez de adicionar a exportação a todas as classes dessa hierarquia, você decidiu criar uma nova classe, externa à hierarquia, e colocar toda a lógica de exportação dentro dela. A classe teria métodos para exportar o estado público de cada objeto em strings XML:
class Exporter is
method export(s: Shape) is
print("Exporting shape")
method export(d: Dot)
print("Exporting dot")
method export(c: Circle)
print("Exporting circle")
method export(r: Rectangle)
print("Exporting rectangle")
method export(cs: CompoundGraphic)
print("Exporting compound")
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 vai imprimir "Exporting shape".
Espere! Por quê?!
Pensando como um compilador
Observação: as informações a seguir são verdadeiras para a maioria das linguagens de programação orientadas a objetos modernas (Java, C#, PHP e outras).
Vinculação tardia/dinâmica
Imagine que você é um compilador. Você tem que decidir como compilar o código a seguir:
method drawShape(shape: Shape) is
shape.draw();
Vejamos... o método draw está definido na classe Shape. Espere um segundo, mas também há quatro subclasses que sobrescrevem esse método. Conseguimos decidir com segurança qual das implementações chamar aqui? Não parece. A única forma de saber com certeza é executar o programa e verificar a classe de um objeto passado para o método. A única coisa que sabemos com certeza é que o objeto terá uma implementação do método draw.
Portanto, o código de máquina resultante verificará a classe do objeto passado para o parâmetro shape e escolherá a implementação de draw da classe apropriada.
Essa verificação dinâmica de tipo é chamada de vinculação tardia (ou dinâmica):
- Tardia, porque vinculamos o objeto e sua implementação após a compilação, em tempo de execução.
- Dinâmica, porque cada novo objeto pode precisar ser vinculado a uma implementação diferente.
Vinculação inicial/estática
Agora, vamos “compilar” o código a seguir:
method exportShape(shape: Shape) is
Exporter exporter = new Exporter()
exporter.export(shape);
A segunda linha está clara: a classe Exporter não tem um construtor personalizado, então simplesmente instanciamos um objeto. E quanto à chamada export? O Exporter tem cinco métodos com o mesmo nome que diferem nos tipos de parâmetro. Qual deles chamar? Parece que 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 tenha um método export apropriado na classe Exporter? Por exemplo, um objeto Ellipse. O compilador não consegue garantir que o método sobrecarregado apropriado exista, ao contrário dos métodos sobrescritos. Surge uma situação ambígua que o compilador não pode permitir.
Por isso, os desenvolvedores de compiladores seguem um caminho seguro e usam a vinculação inicial (ou estática) para métodos sobrecarregados:
- Inicial porque acontece em tempo de compilação, antes de o programa ser iniciado.
- Estática porque não pode ser alterada em tempo de execução.
Voltemos ao nosso exemplo. Temos certeza de que o argumento recebido será da hierarquia Shape: ou a classe Shape ou uma de suas subclasses. 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 vinculada com segurança a um dado código sem tornar as coisas ambíguas. É por isso que, mesmo que passemos um objeto Rectangle para exportShape, o exportador ainda chamará um método export(s: Shape).
Double dispatch
Double dispatch (despacho duplo) é um truque que permite usar vinculação dinâmica junto com métodos sobrecarregados. Veja como isso é feito:
class Visitor is
method visit(s: Shape) is
print("Visited shape")
method visit(d: Dot)
print("Visited dot")
interface Graphic is
method accept(v: Visitor)
class Shape implements Graphic is
method accept(v: Visitor)
// O compilador sabe com certeza que `this` é um `Shape`.
// O que significa que `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 `visit(s: Dot)` pode ser chamado com segurança.
v.visit(this)
Visitor v = new Visitor();
Graphic g = new Dot();
// O método `accept` é sobrescrito, não sobrecarregado. O compilador o vincula
// dinamicamente. Portanto, o `accept` será executado em uma classe que
// corresponde a um objeto que chama um método (no nosso caso, a classe `Dot`).
g.accept(v);
// Saída: "Visited dot"
Posfácio
Embora o padrão Visitor seja construído sobre o princípio do double dispatch, esse não é seu propósito principal. O Visitor permite adicionar operações “externas” a toda uma hierarquia de classes sem alterar o código existente dessas classes.