O livro "JAVA and Patterns" do Dr. Yan Hong começa com uma descrição do padrão Visitor:
Padrão de visitante é o padrão de comportamento dos objetos. O objetivo do padrão visitante é encapsular algumas operações que são aplicadas a determinados elementos da estrutura de dados. Uma vez que essas operações precisam ser modificadas, a estrutura de dados que aceita esta operação pode permanecer inalterada.
O conceito de expedição
O tipo quando uma variável é declarada é chamado de tipo estático da variável (tipo estático), e algumas pessoas chamam o tipo estático de tipo aparente (tipo aparente) e o tipo real do objeto referenciado pela variável também é chamado de tipo aparente; tipo real da variável (Actual Type). por exemplo:
Copie o código do código da seguinte forma:
Lista lista = null;
lista = new ArrayList();
Uma lista de variáveis é declarada, seu tipo estático (também chamado de tipo óbvio) é List e seu tipo real é ArrayList.
A seleção de métodos com base no tipo de objeto é o despacho dividido em dois tipos, nomeadamente despacho estático e despacho dinâmico.
O envio estático ocorre em tempo de compilação e o envio ocorre com base em informações de tipo estático. O despacho estático não é estranho para nós. A sobrecarga do método é despacho estático.
O despacho dinâmico ocorre durante o tempo de execução e o despacho dinâmico substitui dinamicamente um método.
expedição estática
Java suporta envio estático por meio de sobrecarga de método. Usando a história de Mozi cavalgando como exemplo, Mozi poderia montar um cavalo branco ou um cavalo preto. O diagrama de classes de Mozi e cavalo branco, cavalo preto e cavalo é o seguinte:
Neste sistema, Mozi é representado pela classe Mozi. O código é o seguinte:
classe pública Mozi {
passeio vazio público (Cavalo h){
System.out.println("passeios a cavalo");
}
passeio vazio público (WhiteHorse wh){
System.out.println("montando um cavalo branco");
}
passeio vazio público (BlackHorse bh){
System.out.println("Cavalgue o azarão");
}
public static void main(String[] args) {
Cavalo wh = new WhiteHorse();
Cavalo bh = new BlackHorse();
Mozi mozi = novo Mozi();
mozi.ride(wh);
mozi.ride(bh);
}
}
Obviamente, o método ride() da classe Mozi está sobrecarregado com três métodos. Esses três métodos aceitam parâmetros de Horse, WhiteHorse, BlackHorse e outros tipos, respectivamente.
Então, quais resultados o programa imprimirá durante a execução? O resultado é que o programa imprime as mesmas duas linhas de “cavalo”. Em outras palavras, Mozi descobriu que tudo o que ele montava eram cavalos.
Por que? As duas chamadas ao método ride() passam parâmetros diferentes, nomeadamente wh e bh. Embora tenham tipos reais diferentes, seus tipos estáticos são todos iguais, que são tipos Cavalo.
O envio de métodos sobrecarregados é baseado em tipos estáticos e esse processo de envio é concluído em tempo de compilação.
despacho dinâmico
Java oferece suporte ao envio dinâmico por meio da substituição de métodos. Usando a história de um cavalo comendo grama como exemplo, o código é o seguinte:
Copie o código do código da seguinte forma:
classe pública Cavalo {
public void comer(){
System.out.println("Cavalo comendo grama");
}
}
Copie o código do código da seguinte forma:
classe pública BlackHorse estende Cavalo {
@Substituir
public void comer() {
System.out.println("Azarão comendo grama");
}
}
Copie o código do código da seguinte forma:
classe pública Cliente {
public static void main(String[] args) {
Cavalo h = new BlackHorse();
aquecer();
}
}
O tipo estático da variável h é Horse e o tipo real é BlackHorse. Se o método eat() na última linha acima chama o método eat() da classe BlackHorse, então o que está impresso acima é "Black Horse Eating Grass" pelo contrário, se o método eat() acima chama o método eat(; ) da classe Horse , então o que é impresso é "cavalo come grama".
Portanto, o cerne do problema é que o compilador Java nem sempre sabe qual código será executado durante a compilação, porque o compilador conhece apenas o tipo estático do objeto, mas não conhece o tipo real do objeto e do método; a chamada é baseada nos tipos reais do objeto, não nos tipos estáticos. Desta forma, o método eat() na última linha acima chama o método eat() da classe BlackHorse e imprime “cavalo preto comendo grama”.
tipo de envio
O objeto ao qual um método pertence é chamado de receptor do método. O receptor do método e os parâmetros do método são chamados coletivamente de volume do método. Por exemplo, o código de cópia da classe Test no exemplo abaixo é o seguinte:
teste de classe pública {
impressão pública void(String str){
System.out.println(str);
}
}
Na classe acima, o método print() pertence ao objeto Test, portanto seu receptor também é o objeto Test. O método print() possui um parâmetro chamado str e seu tipo é String.
Dependendo de quantos tipos de despacho de quantidades podem ser baseados, as linguagens orientadas a objetos podem ser divididas em linguagens de despacho único (Uni-Dispatch) e linguagens de despacho múltiplo (Multi-Dispatch). As linguagens de despacho único selecionam métodos com base no tipo de uma instância, enquanto as linguagens de despacho múltiplo selecionam métodos com base no tipo de mais de uma instância.
Tanto C++ quanto Java são linguagens de despacho único, e exemplos de linguagens de despacho múltiplo incluem CLOS e Cecil. De acordo com esta distinção, Java é uma linguagem dinâmica de despacho único, porque o despacho dinâmico desta linguagem leva em consideração apenas o tipo do receptor do método, e é uma linguagem estática de despacho único, porque esta linguagem despacha métodos sobrecarregados. são levados em consideração o tipo de receptor do método e os tipos de todos os parâmetros do método.
Em uma linguagem que suporta despacho único dinâmico, existem duas condições que determinam qual operação uma solicitação chamará: uma é o nome da solicitação e o tipo real do receptor. O despacho único limita o processo de seleção do método de forma que apenas uma instância possa ser considerada, que geralmente é o receptor do método. Na linguagem Java, se uma operação for executada em um objeto de tipo desconhecido, o teste de tipo real do objeto ocorrerá apenas uma vez. Esta é a característica do despacho único dinâmico.
despacho duplo
Um método decide executar código diferente com base nos tipos de duas variáveis. Isso é "duplo envio". A linguagem Java não suporta despacho múltiplo dinâmico, o que significa que Java não suporta despacho duplo dinâmico. Mas usando padrões de design, o despacho duplo dinâmico também pode ser implementado na linguagem Java.
Em Java, dois despachos podem ser alcançados por meio de duas chamadas de método. O diagrama de classes é o seguinte:
Existem dois objetos na imagem, o da esquerda é chamado de Oeste e o da direita é chamado de Leste. Agora, o objeto West primeiro chama o método goEast() do objeto East, passando-se. Quando o objeto East é chamado, ele sabe imediatamente quem é o chamador com base nos parâmetros passados, então o método goWest() do objeto "chamador" é chamado por sua vez. Através de duas chamadas, o controle do programa é transferido para dois objetos. O diagrama de sequência é o seguinte:
Dessa forma, há duas chamadas de método. O controle do programa é passado entre os dois objetos. Primeiro, ele é passado do objeto Oeste para o objeto Leste e depois é passado de volta para o objeto Oeste.
Mas apenas devolver a bola não resolve o problema da dupla distribuição. A chave é como usar essas duas chamadas e a função dinâmica de despacho único da linguagem Java para acionar dois despachos únicos durante esse processo de passagem.
O despacho único dinâmico na linguagem Java ocorre quando uma subclasse substitui um método de uma classe pai. Em outras palavras, tanto West quanto East devem ser colocados em sua própria hierarquia de tipos, conforme mostrado abaixo:
código fonte
O código de cópia da classe West é o seguinte:
classe abstrata pública Oeste {
público abstrato void goWest1 (SubEast1 leste);
público abstrato void goWest2 (SubEast2 leste);
}
O código de cópia da classe SubWest1 é o seguinte:
classe pública SubWest1 estende West{
@Substituir
public void goWest1(SubEast1 leste) {
System.out.println("SubWest1 + " + east.myName1());
}
@Substituir
public void goWest2(SubEast2 leste) {
System.out.println("SubWest1 + " + east.myName2());
}
}
SubOeste Classe 2
Copie o código do código da seguinte forma:
classe pública SubWest2 estende West{
@Substituir
public void goWest1(SubEast1 leste) {
System.out.println("SubWest2 + " + east.myName1());
}
@Substituir
public void goWest2(SubEast2 leste) {
System.out.println("SubWest2 + " + east.myName2());
}
}
O código de cópia da classe Leste é o seguinte:
classe abstrata pública Leste {
público abstrato void goEast(Oeste oeste);
}
O código de cópia da classe SubEast1 é o seguinte:
classe pública SubEast1 estende Leste{
@Substituir
public void goEast(Oeste oeste) {
oeste.goWest1(este);
}
public String meuNome1(){
retornar "SubLeste1";
}
}
O código de cópia da classe SubEast2 é o seguinte:
classe pública SubEast2 estende Leste{
@Substituir
public void goEast(Oeste oeste) {
oeste.goWest2(este);
}
public String meuNome2(){
retornar "SubLeste2";
}
}
O código de cópia da classe cliente é o seguinte:
classe pública Cliente {
public static void main(String[] args) {
//combinação 1
Leste leste = new SubEast1();
Oeste oeste = new SubWest1();
leste.goLeste(oeste);
//combinação 2
leste = new SubLeste1();
oeste = novo SubOeste2();
leste.goLeste(oeste);
}
}
Os resultados da execução são os seguintes.
SubOeste1 + SubLeste1
SubOeste2 + SubLeste1
Quando o sistema está em execução, os objetos SubWest1 e SubEast1 são criados primeiro e, em seguida, o cliente chama o método goEast() de SubEast1 e passa o objeto SubWest1. Como o objeto SubEast1 substitui o método goEast() de sua superclasse East, um único despacho dinâmico ocorre neste momento. Quando o objeto SubEast1 recebe a chamada, ele obterá o objeto SubWest1 do parâmetro, então ele imediatamente chama o método goWest1() deste objeto e se transmite. Como o objeto SubEast1 tem o direito de escolher qual objeto chamar, outro despacho de método dinâmico é executado neste momento.
Neste momento, o objeto SubWest1 obteve o objeto SubEast1. Ao chamar o método myName1() deste objeto, você pode imprimir seu próprio nome e o nome do objeto SubEast. O diagrama de sequência é o seguinte:
Como um desses dois nomes vem da hierarquia Oriental e o outro vem da hierarquia Ocidental, a sua combinação é determinada dinamicamente. Este é o mecanismo de implementação do despacho duplo dinâmico.
A estrutura do padrão de visitante
O padrão visitante é adequado para sistemas com estruturas de dados relativamente indeterminadas. Ele desacopla o acoplamento entre a estrutura de dados e as operações que atuam na estrutura, permitindo que o conjunto de operações evolua de forma relativamente livre. Um diagrama simplificado do padrão de visitante é mostrado abaixo:
Cada nó da estrutura de dados pode aceitar uma chamada de um visitante. Este nó passa o objeto do nó para o objeto do visitante, e o objeto do visitante, por sua vez, executa as operações do objeto do nó. Este processo é denominado "despacho duplo". O nó chama o visitante, passando-se, e o visitante executa um algoritmo nesse nó. Um diagrama de classe esquemático para o padrão Visitante é mostrado abaixo:
As funções envolvidas no modo visitante são as seguintes:
● Função de visitante abstrato (Visitor) : declara uma ou mais operações de método para formar a interface que todas as funções específicas de visitante devem implementar.
● Papel Visitante Concreto (ConcreteVisitor) : implementa a interface declarada pelo visitante abstrato, ou seja, cada operação de acesso declarada pelo visitante abstrato.
● Função do nó abstrato (Nó) : declara uma operação de aceitação e aceita um objeto visitante como parâmetro.
● Função ConcreteNode : implementa a operação de aceitação especificada pelo nó abstrato.
● Função de objeto de estrutura (ObjectStructure) : tem as seguintes responsabilidades, pode percorrer todos os elementos da estrutura se necessário, fornecer uma interface de alto nível para que os objetos visitantes possam acessar cada elemento, se necessário, podem ser projetados como um objeto composto; Uma coleção, como List ou Set.
código fonte
Como você pode ver, a função abstrata de visitante prepara uma operação de acesso para cada nó específico. Como existem dois nós, existem duas operações de acesso correspondentes.
Copie o código do código da seguinte forma:
Visitante de interface pública {
/**
*Corresponde à operação de acesso do NodeA
*/
visita pública nula (nó NodeA);
/**
*Corresponde à operação de acesso do NodeB
*/
visita pública nula (nó NodeB);
}
O código de cópia da classe visitanteA específico é o seguinte:
classe pública VisitorA implementa Visitante {
/**
*Corresponde à operação de acesso do NodeA
*/
@Substituir
visita pública nula (nó NodeA) {
System.out.println(node.operaçãoA());
}
/**
*Corresponde à operação de acesso do NodeB
*/
@Substituir
visita pública nula (nó NodeB) {
System.out.println(node.operaçãoB());
}
}
O código de cópia da classe VisitorB do visitante específico é o seguinte:
classe pública VisitorB implementa Visitante {
/**
*Corresponde à operação de acesso do NodeA
*/
@Substituir
visita pública nula (nó NodeA) {
System.out.println(node.operaçãoA());
}
/**
*Corresponde à operação de acesso do NodeB
*/
@Substituir
visita pública nula (nó NodeB) {
System.out.println(node.operaçãoB());
}
}
O código de cópia da classe de nó abstrato é o seguinte:
classe abstrata pública Nó {
/**
* Aceitar operação
*/
público abstrato void aceitar (Visitante visitante);
}
Classe de nó específica NodeA
Copie o código do código da seguinte forma:
classe pública NodeA estende Node{
/**
* Aceitar operação
*/
@Substituir
public void aceitar(Visitante visitante) {
visitante.visita(este);
}
/**
*Método específico do NodeA
*/
operação de string públicaA(){
retornar "NóA";
}
}
Classe de nó específica NodeB
Copie o código do código da seguinte forma:
classe pública NodeB estende Node{
/**
*Aceitar método
*/
@Substituir
public void aceitar(Visitante visitante) {
visitante.visita(este);
}
/**
*Métodos específicos do NodeB
*/
operação de string públicaB(){
retornar "NóB";
}
}
Classe de função de objeto estrutural. Esta função de objeto estrutural contém uma coleção e fornece o método add() para o mundo externo como uma operação de gerenciamento para a coleção. Ao chamar este método, um novo nó pode ser adicionado dinamicamente.
Copie o código do código da seguinte forma:
classe pública Estrutura de Objeto {
private List<Nó> nós = new ArrayList<Node>();
/**
* Executar operação do método
*/
ação pública nula(Visitante visitante){
for (nó nó: nós)
{
nó.accept(visitante);
}
}
/**
* Adicione um novo elemento
*/
public void add(Nó nó){
nós.add(nó);
}
}
O código de cópia da classe cliente é o seguinte:
classe pública Cliente {
public static void main(String[] args) {
//Cria um objeto de estrutura
ObjectStructure os = new ObjectStructure();
//Adiciona um nó à estrutura
os.add(novo NodeA());
//Adiciona um nó à estrutura
os.add(novo NodeB());
//Cria um visitante
Visitante visitante = new VisitorA();
os.action(visitante);
}
}
Embora uma estrutura de árvore de objetos complexa com vários nós de ramificação não apareça nesta implementação esquemática, em sistemas reais o padrão de visitante é geralmente usado para lidar com estruturas de árvore de objetos complexas, e o padrão de visitante pode ser usado para lidar com problemas de estrutura de árvore que abrangem múltiplas hierarquias . É aqui que o padrão de visitante é tão poderoso.
Diagrama de sequência do processo de preparação
Primeiro, este cliente ilustrativo cria um objeto de estrutura e depois passa um novo objeto NodeA e um novo objeto NodeB.
Em segundo lugar, o cliente cria um objeto VisitorA e passa esse objeto para o objeto de estrutura.
Em seguida, o cliente chama o método de gerenciamento de agregação de objetos de estrutura para adicionar os nós NodeA e NodeB ao objeto de estrutura.
Por fim, o cliente chama o método de ação action() do objeto de estrutura para iniciar o processo de acesso.
Diagrama de sequência do processo de acesso
O objeto de estrutura percorrerá todos os nós da coleção que salva, que neste sistema são os nós NodeA e NodeB. Primeiro, o NodeA será acessado. Este acesso consiste nas seguintes operações:
(1) O método accept() do objeto NodeA é chamado e o próprio objeto VisitorA é passado;
(2) O objeto NodeA, por sua vez, chama o método de acesso do objeto VisitorA e passa o próprio objeto NodeA;
(3) O objeto VisitorA chama o método exclusivo operationA() do objeto NodeA.
Assim, o processo de duplo despacho é concluído. Então, o NodeB será acessado. O processo de acesso é igual ao processo de acesso do NodeA, que não será descrito aqui.
Vantagens do padrão de visitante
● Uma boa extensibilidade pode adicionar novas funções aos elementos da estrutura do objeto sem modificar os elementos da estrutura do objeto.
● Uma boa capacidade de reutilização permite aos visitantes definir funções comuns a toda a estrutura do objeto, melhorando assim o grau de reutilização.
● Separando comportamentos irrelevantes Você pode usar visitantes para separar comportamentos irrelevantes e encapsular comportamentos relacionados para formar um visitante, de modo que a função de cada visitante seja relativamente única.
Desvantagens do padrão de visitante
● É difícil alterar a estrutura do objeto. Não é adequado para situações em que as classes na estrutura do objeto mudam com frequência. Como a estrutura do objeto muda, a interface do visitante e a implementação do visitante devem mudar de acordo, o que é muito caro.
● Quebrar o padrão Encapsulation Visitor geralmente requer que a estrutura do objeto abra dados internos para visitantes e ObjectStructrue, que quebra o encapsulamento do objeto.