O envio duplo é apenas uma das razões para usar esse padrão .
Mas observe que é a única maneira de implementar o envio duplo ou mais em idiomas que usa um único paradigma de envio.
Aqui estão os motivos para usar o padrão:
1) Queremos definir novas operações sem alterar o modelo a cada momento, porque o modelo não muda frequentemente, enquanto as operações mudam com frequência.
2) Não queremos unir modelo e comportamento porque queremos ter um modelo reutilizável em vários aplicativos ou queremos ter um modelo extensível que permita que as classes clientes definam seus comportamentos com suas próprias classes.
3) Temos operações comuns que dependem do tipo concreto do modelo, mas não queremos implementar a lógica em cada subclasse, pois isso explodiria a lógica comum em várias classes e, portanto, em vários lugares .
4) Estamos usando um design de modelo de domínio e as classes de modelo da mesma hierarquia executam muitas coisas distintas que poderiam ser reunidas em outro lugar .
5) Precisamos de uma expedição dupla .
Temos variáveis declaradas com tipos de interface e queremos poder processá-las de acordo com o tipo de tempo de execução ... é claro, sem usar if (myObj instanceof Foo) {}
nenhum truque.
A idéia é, por exemplo, passar essas variáveis para métodos que declaram um tipo concreto da interface como parâmetro para aplicar um processamento específico. Essa maneira de fazer não é possível imediatamente, com idiomas, depende de um único despacho, porque o escolhido invocado no tempo de execução depende apenas do tipo de tempo de execução do receptor.
Observe que em Java, o método (assinatura) a ser chamado é escolhido no momento da compilação e depende do tipo declarado dos parâmetros, não do tipo de tempo de execução.
O último ponto que é um motivo para usar o visitante também é uma conseqüência, porque à medida que você implementa o visitante (é claro para idiomas que não suportam o envio múltiplo), você precisa necessariamente introduzir uma implementação de envio duplo.
Observe que a passagem de elementos (iteração) para aplicar o visitante em cada um deles não é um motivo para usar o padrão.
Você usa o padrão porque divide o modelo e o processamento.
E, usando o padrão, você se beneficia, além de uma capacidade de iterador.
Essa capacidade é muito poderosa e vai além da iteração no tipo comum com um método específico, como accept()
é um método genérico.
É um caso de uso especial. Então, vou colocar isso de lado.
Exemplo em Java
Ilustrarei o valor agregado do padrão com um exemplo de xadrez no qual gostaríamos de definir o processamento à medida que o jogador solicita uma peça em movimento.
Sem o uso do padrão de visitante, poderíamos definir comportamentos de movimentação de peças diretamente nas subclasses de peças.
Poderíamos ter, por exemplo, uma Piece
interface como:
public interface Piece{
boolean checkMoveValidity(Coordinates coord);
void performMove(Coordinates coord);
Piece computeIfKingCheck();
}
Cada subclasse de Piece o implementaria como:
public class Pawn implements Piece{
@Override
public boolean checkMoveValidity(Coordinates coord) {
...
}
@Override
public void performMove(Coordinates coord) {
...
}
@Override
public Piece computeIfKingCheck() {
...
}
}
E o mesmo para todas as subclasses de Piece.
Aqui está uma classe de diagrama que ilustra esse design:
Essa abordagem apresenta três desvantagens importantes:
- comportamentos como performMove()
ou computeIfKingCheck()
provavelmente usarão lógica comum.
Por exemplo, qualquer que seja o concreto Piece
, performMove()
finalmente definirá a peça atual para um local específico e potencialmente levará a peça oponente.
A divisão de comportamentos relacionados em várias classes, em vez de reuni-los, derrota de alguma maneira o padrão de responsabilidade único. Tornando sua manutenção mais difícil.
- processar como checkMoveValidity()
não deve ser algo que as Piece
subclasses possam ver ou mudar.
É o cheque que vai além das ações humanas ou do computador. Essa verificação é realizada em cada ação solicitada por um jogador para garantir que o movimento da peça solicitada seja válido.
Portanto, nem queremos fornecer isso na Piece
interface.
- Em jogos de xadrez desafiadores para desenvolvedores de bot, geralmente o aplicativo fornece uma API padrão ( Piece
interfaces, subclasses, tabuleiro, comportamentos comuns, etc.) e permite que os desenvolvedores aprimorem sua estratégia de bot.
Para poder fazer isso, precisamos propor um modelo em que dados e comportamentos não sejam fortemente acoplados nas Piece
implementações.
Então, vamos usar o padrão de visitante!
Temos dois tipos de estrutura:
- as classes de modelo que aceitam serem visitadas (as peças)
- os visitantes que os visitam (operações móveis)
Aqui está um diagrama de classes que ilustra o padrão:
Na parte superior, temos os visitantes e na parte inferior, temos as classes de modelo.
Aqui está a PieceMovingVisitor
interface (comportamento especificado para cada tipo de Piece
):
public interface PieceMovingVisitor {
void visitPawn(Pawn pawn);
void visitKing(King king);
void visitQueen(Queen queen);
void visitKnight(Knight knight);
void visitRook(Rook rook);
void visitBishop(Bishop bishop);
}
A peça está definida agora:
public interface Piece {
void accept(PieceMovingVisitor pieceVisitor);
Coordinates getCoordinates();
void setCoordinates(Coordinates coordinates);
}
Seu método principal é:
void accept(PieceMovingVisitor pieceVisitor);
Ele fornece o primeiro despacho: uma chamada baseada no Piece
receptor.
Em tempo de compilação, o método é vinculado ao accept()
método da interface Piece e, em tempo de execução, o método limitado será chamado na Piece
classe de tempo de execução .
E é a accept()
implementação do método que executará um segundo envio.
De fato, cada Piece
subclasse que deseja ser visitada por um PieceMovingVisitor
objeto invoca o PieceMovingVisitor.visit()
método passando como o próprio argumento.
Dessa forma, o compilador limita, assim que o tempo de compilação, o tipo do parâmetro declarado com o tipo concreto.
Há o segundo despacho.
Aqui está a Bishop
subclasse que ilustra que:
public class Bishop implements Piece {
private Coordinates coord;
public Bishop(Coordinates coord) {
super(coord);
}
@Override
public void accept(PieceMovingVisitor pieceVisitor) {
pieceVisitor.visitBishop(this);
}
@Override
public Coordinates getCoordinates() {
return coordinates;
}
@Override
public void setCoordinates(Coordinates coordinates) {
this.coordinates = coordinates;
}
}
E aqui um exemplo de uso:
// 1. Player requests a move for a specific piece
Piece piece = selectPiece();
Coordinates coord = selectCoordinates();
// 2. We check with MoveCheckingVisitor that the request is valid
final MoveCheckingVisitor moveCheckingVisitor = new MoveCheckingVisitor(coord);
piece.accept(moveCheckingVisitor);
// 3. If the move is valid, MovePerformingVisitor performs the move
if (moveCheckingVisitor.isValid()) {
piece.accept(new MovePerformingVisitor(coord));
}
Desvantagens do visitante
O padrão Visitor é um padrão muito poderoso, mas também possui algumas limitações importantes que você deve considerar antes de usá-lo.
1) Risco de reduzir / quebrar o encapsulamento
Em alguns tipos de operação, o padrão de visitante pode reduzir ou interromper o encapsulamento de objetos de domínio.
Por exemplo, como a MovePerformingVisitor
classe precisa definir as coordenadas da peça real, a Piece
interface deve fornecer uma maneira de fazer isso:
void setCoordinates(Coordinates coordinates);
A responsabilidade das Piece
alterações de coordenadas está agora aberta a outras classes além das Piece
subclasses.
Mover o processamento executado pelo visitante nas Piece
subclasses também não é uma opção.
De fato, criará outro problema, pois Piece.accept()
aceita qualquer implementação de visitante. Ele não sabe o que o visitante executa e, portanto, não faz ideia sobre se e como alterar o estado da Peça.
Uma maneira de identificar o visitante seria realizar um pós-processamento de Piece.accept()
acordo com a implementação do visitante. Seria uma péssima idéia, pois criaria um alto acoplamento entre as implementações do Visitor e as subclasses de Piece e, além disso, provavelmente exigiria o uso de truques como getClass()
, instanceof
ou qualquer marcador que identifique a implementação do Visitor.
2) Requisito para alterar o modelo
Ao contrário de outros padrões de design comportamental, Decorator
por exemplo, o padrão de visitantes é intrusivo.
De fato, precisamos modificar a classe do receptor inicial para fornecer um accept()
método para aceitar a visita.
Não tivemos nenhum problema para Piece
e suas subclasses, pois essas são nossas classes .
Nas aulas integradas ou de terceiros, as coisas não são tão fáceis.
Precisamos envoltório ou herdar (se podemos)-los para adicionar o accept()
método.
3) Indirecionamentos
O padrão cria múltiplos indiretos.
O despacho duplo significa duas invocações em vez de uma única:
call the visited (piece) -> that calls the visitor (pieceMovingVisitor)
E poderíamos ter indiretos adicionais à medida que o visitante altera o estado do objeto visitado.
Pode parecer um ciclo:
call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)