Fala-se muito sobre separar os algoritmos das classes. Mas, uma coisa fica de lado não explicada.
Eles usam visitante assim
abstract class Expr {
public <T> T accept(Visitor<T> visitor) {visitor.visit(this);}
}
class ExprVisitor extends Visitor{
public Integer visit(Num num) {
return num.value;
}
public Integer visit(Sum sum) {
return sum.getLeft().accept(this) + sum.getRight().accept(this);
}
public Integer visit(Prod prod) {
return prod.getLeft().accept(this) * prod.getRight().accept(this);
}
Em vez de chamar visit (elemento) diretamente, Visitor pede ao elemento para chamar seu método visit. Isso contradiz a ideia declarada de desconhecimento da classe sobre os visitantes.
PS1 Explique com suas próprias palavras ou aponte a explicação exata. Porque duas respostas que obtive referem-se a algo geral e incerto.
PS2 Meu palpite: Como getLeft()
retorna o básico Expression
, chamandovisit(getLeft())
resultaria em visit(Expression)
, enquanto a getLeft()
chamada visit(this)
resultaria em outra invocação de visita mais apropriada. Então, accept()
realiza a conversão de tipo (também conhecido como casting).
PS3 Scala's Pattern Matching = Visitor Pattern on Steroid mostra como o padrão Visitor é muito mais simples sem o método accept. A Wikipedia acrescenta a esta afirmação : vinculando um artigo mostrando "que os accept()
métodos são desnecessários quando a reflexão está disponível; introduz o termo 'Walkabout' para a técnica."
Respostas:
O padrão do visitante
visit
/accept
construções são um mal necessário devido à semântica das linguagens C-like (C #, Java, etc.). O objetivo do padrão de visitante é usar o despacho duplo para rotear sua chamada como você esperaria da leitura do código.Normalmente, quando o padrão de visitante é usado, uma hierarquia de objeto está envolvida em que todos os nós são derivados de um
Node
tipo base , denominado doravanteNode
. Instintivamente, escreveríamos assim:Aqui está o problema. Se nossa
MyVisitor
classe foi definida da seguinte forma:Se, em tempo de execução, independentemente do tipo real
root
, nossa chamada iria para a sobrecargavisit(Node node)
. Isso seria verdadeiro para todas as variáveis declaradas do tipoNode
. Por que é isso? Porque Java e outras linguagens semelhantes a C consideram apenas o tipo estático , ou o tipo como a variável é declarada, do parâmetro ao decidir qual sobrecarga chamar. Java não dá o passo extra para perguntar, para cada chamada de método, em tempo de execução, "Ok, qual é o tipo dinâmico deroot
? Ah, entendo. É aTrainNode
. Vamos ver se há algum métodoMyVisitor
que aceita um parâmetro do tipoTrainNode
... ". O compilador, em tempo de compilação, determina qual é o método que será chamado. (Se o Java realmente inspecionasse os tipos dinâmicos dos argumentos, o desempenho seria péssimo.)Java nos dá uma ferramenta para levar em consideração o tipo de tempo de execução (ou seja, dinâmico) de um objeto quando um método é chamado - envio de método virtual . Quando chamamos um método virtual, a chamada realmente vai para uma tabela na memória que consiste em ponteiros de função. Cada tipo possui uma mesa. Se um método específico for substituído por uma classe, a entrada da tabela de funções dessa classe conterá o endereço da função substituída. Se a classe não sobrescrever um método, ela conterá um ponteiro para a implementação da classe base. Isso ainda incorre em uma sobrecarga de desempenho (cada chamada de método basicamente desreferencia dois ponteiros: um apontando para a tabela de funções do tipo e outro da própria função), mas ainda é mais rápido do que inspecionar os tipos de parâmetros.
O objetivo do padrão de visitante é realizar o despacho duplo - não apenas o tipo de destino da chamada é considerado (
MyVisitor
por meio de métodos virtuais), mas também o tipo do parâmetro (que tipoNode
estamos olhando)? O padrão Visitor nos permite fazer isso pela combinaçãovisit
/accept
.Mudando nossa linha para esta:
Podemos obter o que queremos: via envio de método virtual, inserimos a chamada aceita () correta conforme implementada pela subclasse - em nosso exemplo com
TrainElement
, inseriremosTrainElement
a implementação deaccept()
:O que faz o know compilador neste momento, dentro do âmbito da
TrainNode
'saccept
? Sabe que o tipo estático dethis
é aTrainNode
. Este é um fragmento adicional importante de informação que o compilador não estava ciente no escopo de nosso chamador: lá, tudo o que ele sabiaroot
era que era umNode
. Agora, o compilador sabe quethis
(root
) não é apenas umNode
, mas na verdade é umTrainNode
. Em conseqüência, a única linha encontrada dentro deaccept()
:,v.visit(this)
significa algo totalmente diferente. O compilador agora procurará por uma sobrecarga devisit()
que leva aTrainNode
. Se não conseguir encontrar um, ele irá compilar a chamada para uma sobrecarga que leva umNode
. Se nenhum dos dois existir, você obterá um erro de compilação (a menos que haja uma sobrecargaobject
). A execução entrará, portanto, no que pretendíamos o tempo todo:MyVisitor
a implementação devisit(TrainNode e)
. Nenhum molde foi necessário e, mais importante, nenhuma reflexão foi necessária. Portanto, a sobrecarga desse mecanismo é bastante baixa: ele consiste apenas em referências de ponteiro e nada mais.Você está certo em sua pergunta - podemos usar um gesso e obter o comportamento correto. No entanto, muitas vezes, nem sabemos que tipo de nó é. Considere o caso da seguinte hierarquia:
E estávamos escrevendo um compilador simples que analisa um arquivo de origem e produz uma hierarquia de objetos que está em conformidade com a especificação acima. Se estivéssemos escrevendo um intérprete para a hierarquia implementada como Visitante:
O casting não nos levaria muito longe, pois não conhecemos os tipos de
left
ouright
nosvisit()
métodos. Nosso analisador provavelmente também retornaria um objeto do tipoNode
que também apontasse para a raiz da hierarquia, portanto, também não podemos lançar isso com segurança. Portanto, nosso intérprete simples pode ser semelhante a:O padrão de visitante nos permite fazer algo muito poderoso: dada uma hierarquia de objeto, ele nos permite criar operações modulares que operam sobre a hierarquia sem a necessidade de colocar o código na própria classe da hierarquia. O padrão de visitante é amplamente usado, por exemplo, na construção do compilador. Dada a árvore de sintaxe de um programa específico, muitos visitantes são escritos para operar nessa árvore: verificação de tipo, otimizações, emissão de código de máquina são geralmente implementados como visitantes diferentes. No caso do visitante de otimização, ele pode até gerar uma nova árvore de sintaxe dada a árvore de entrada.
Isso tem suas desvantagens, é claro: se adicionarmos um novo tipo à hierarquia, precisamos também adicionar um
visit()
método para esse novo tipo naIVisitor
interface e criar implementações stub (ou completas) em todos os nossos visitantes. Também precisamos adicionar oaccept()
método, pelas razões descritas acima. Se desempenho não significa muito para você, existem soluções para escrever visitantes sem precisar doaccept()
, mas normalmente envolvem reflexão e, portanto, podem incorrer em uma grande sobrecarga.fonte
accept()
método se torna necessário quando esse aviso é violado no Visitante.Claro que isso seria idiota se essa fosse a única maneira de o Accept ser implementado.
Mas não é.
Por exemplo, os visitantes são realmente úteis ao lidar com hierarquias, caso em que a implementação de um nó não terminal pode ser algo como este
Entende? O que você descreve como estúpido é a solução para atravessar hierarquias.
Aqui está um artigo muito mais longo e aprofundado que me fez entender o visitante .
Editar: Para esclarecer: O
Visit
método do visitante contém lógica a ser aplicada a um nó. OAccept
método do nó contém lógica sobre como navegar para nós adjacentes. O caso em que você só faz o despacho duplo é um caso especial em que simplesmente não há nós adjacentes para os quais navegar.fonte
O objetivo do padrão Visitor é garantir que os objetos saibam quando o visitante terminou com eles e partiu, para que as classes possam realizar qualquer limpeza necessária posteriormente. Ele também permite que as classes exponham seus internos "temporariamente" como parâmetros 'ref' e saibam que os internos não serão mais expostos quando o visitante se for. Nos casos em que nenhuma limpeza é necessária, o padrão do visitante não é extremamente útil. As classes que não fazem nenhuma dessas coisas podem não se beneficiar do padrão de visitante, mas o código que é escrito para usar o padrão de visitante poderá ser usado com classes futuras que podem exigir limpeza após o acesso.
Por exemplo, suponha que se tenha uma estrutura de dados contendo muitas strings que devem ser atualizadas atomicamente, mas a classe que contém a estrutura de dados não sabe exatamente quais tipos de atualizações atômicas devem ser realizadas (por exemplo, se uma thread deseja substituir todas as ocorrências de " X ", enquanto outro encadeamento deseja substituir qualquer sequência de dígitos por uma seqüência numericamente superior, as operações de ambos os encadeamentos devem ser bem-sucedidas; se cada encadeamento simplesmente ler um string, realizar suas atualizações e escrevê-lo de volta, o segundo encadeamento escrever de volta sua string sobrescreveria a primeira). Uma maneira de fazer isso seria fazer com que cada thread adquira um bloqueio, execute sua operação e libere o bloqueio. Infelizmente, se os bloqueios forem expostos dessa forma,
O padrão Visitante oferece (pelo menos) três abordagens para evitar esse problema:
Sem o padrão de visitante, a execução de atualizações atômicas exigiria a exposição de bloqueios e o risco de falha se o software de chamada não seguir um protocolo de bloqueio / desbloqueio estrito. Com o padrão Visitor, as atualizações atômicas podem ser feitas com relativa segurança.
fonte
Todas as classes que requerem modificação devem implementar o método 'aceitar'. Os clientes chamam esse método de aceitação para realizar alguma nova ação naquela família de classes, estendendo assim sua funcionalidade. Os clientes podem usar este método de aceitação para executar uma ampla gama de novas ações, passando uma classe de visitante diferente para cada ação específica. Uma classe de visitante contém vários métodos de visita substituídos que definem como alcançar a mesma ação específica para cada classe dentro da família. Esses métodos de visita passam uma instância na qual trabalhar.
Os visitantes são úteis se você frequentemente adiciona, altera ou remove funcionalidade de uma família estável de classes, porque cada item de funcionalidade é definido separadamente em cada classe de visitante e as próprias classes não precisam ser alteradas. Se a família de classes não for estável, o padrão do visitante pode ser menos útil, porque muitos visitantes precisam ser alterados sempre que uma classe é adicionada ou removida.
fonte
Um bom exemplo está na compilação do código-fonte:
Os clientes podem implementar um
JavaBuilder
,RubyBuilder
,XMLValidator
, etc. e a implementação de coleta e visitar todos os arquivos de origem em um projeto não precisa de mudança.Este seria um padrão ruim se você tivesse classes separadas para cada tipo de arquivo de origem:
Tudo se resume ao contexto e às partes do sistema que você deseja que sejam extensíveis.
fonte