Estabilidade do visitante x instância de flexibilidade

8

Estou trabalhando em um aplicativo GUI que gera um arquivo de configuração. Eu tenho uma hierarquia de classes para o modelo de configuração e uso uma árvore de objetos dessa hierarquia em vários contextos diferentes. Atualmente, eu uso o padrão Visitor para evitar poluir minhas classes de modelo com código específico de contexto.

interface IConfigurationElement {
    void acceptVisitor(IConfigurationElementVisitor visitor);
}

Em uma versão anterior, usei cadeias de instanceofcondições em vez do Visitor. Comparando as duas abordagens, vejo as seguintes trocas.

Visitante

  • É mais fácil e seguro adicionar novos IConfigurationElement. Basta adicionar uma nova declaração IConfigurationElementVisitore o compilador gera erros para todas as implementações de visitantes. Com instanceofcadeias, você precisa lembrar de todos os lugares que precisa estender com o novo elemento de configuração. Basicamente, instanceofviola o princípio DRY, pois duplica a lógica em vários lugares.
  • O padrão de visitantes é mais eficiente que uma cadeia de instanceofcondições

instancia de

  • A grande vantagem instanceofé a sua flexibilidade. Por exemplo, instanceof permite-me definir soluções especiais para diferentes subconjuntos de IConfigurationElementimplementações que precisam ser tratadas de maneira semelhante em alguns casos. Por outro lado, o Visitor me obriga a implementar um método para cada classe de implementação todas as vezes.

Existe uma solução comum para esse tipo de problema? Posso adaptar o Visitante de alguma forma, para fornecer uma solução comum para alguns casos?

Johannes Luong
fonte
Eu recomendo a você este tutorial do blog, que compara os diferentes estilos de visitantes (instanceOf etc) e fornece respostas muito interessantes. Isso me ajudou. Espero que funcione
AndreasScheinert

Respostas:

1

Você poderia usar o visitante com instanceOf

interfaces:

interface Visitable {
  void accept(Object visitor);
}
interface AVisitor {
  void visitA(A a);
}
interface BVisitor {
  void visitB(B b);
}
interface CVisitor {
  void visitB(C c);
}

Visíveis:

class C implements Visitable {
  public void accept(Object visitor) {
    if (visitor instanceof CVisitor) {
      ((BVisitor)vistor).visitC(this);
    }
  }
}

class B implements Visitable {
  public void accept(Object visitor) {
    if (visitor instanceof BVisitor) {
      ((BVisitor)vistor).visitB(this);
    }
  }
}

class A extends B implements Visitable {
  public void accept(Object visitor) {
    super.accept(visitor);
    if (visitor instanceof AVisitor) {
      ((AVisitor)vistor).visitA(this);
    }
  }
}

Visitantes:

class PrintBs implements BVisitor {
  public void visitB(B b) {
    system.out.println(b);
  }
}

class PrintAs implements AVisitor {
  public void visitA(A a) {
    system.out.println(a);
  }
}

class PrintCs implements CVisitor {
  public void visitC(C c) {
    system.out.println(c);
  }
}
class PrintAsAndCs implements CVisitor, AVisitor{
  public void visitA(A a) {
    system.out.println(a);
  }
  public void visitC(C c) {
    system.out.println(c);
  }
}

cada classe conhece apenas suas interfaces relacionadas; portanto, adicionar novos visitantes ou visitáveis ​​exige a alteração de tudo nessa categoria (visitante / visitável) (para visitante, não requer alteração de nada, para visitável requer a criação de nova interface de visitante, mas, novamente, não mudança de objetos existentes).

Dessa forma, não há uma cadeia de instâncias de testes e o visitante do subconjunto nem precisa saber sobre tipos fora desse subconjunto.

A questão é o que fazer com a situação em que A estende B (e B também é Visível); nesse caso, você pode simplesmente adicionar super.accept (visitante) em accept (portanto, seria uma cadeia curta de instâncias de s, mas apenas como desde que a hierarquia seja profunda e não deva ser muito profunda para importar, e você não precisa escrevê-la inteira manualmente).

user470365
fonte
Se entendi corretamente, você propõe remover IConfigurationElementVisitore apenas verificar tipos específicos de visitantes Visitable. Embora esta seja uma solução possível, vejo algumas desvantagens. Primeiro, isso removeria a estabilidade do Visitor de que falei e, em segundo lugar, atrairia o conhecimento sobre visitantes para minhas Visitableimplementações, o que evitar era parte do motivo para usar o Visitor em primeiro lugar.
Johannes Luong
Não em Visitable (que deve ser apenas uma interface simples com aceitação (Object object)), mas em implementações de Visitable. Portanto, cada classe verifica apenas o visitante, para que saiba apenas o fato de existir uma interface para visitá-lo. Modificarei minha resposta para deixar isso claro.
precisa saber é o seguinte
Ok, sua abordagem permite receber visitantes que visitam apenas subtipos específicos da minha estrutura. Essa é uma boa ideia. O que eu quero é um pouco diferente (eu provavelmente deveria atualizar minha descrição). Eu quero um tempo de compilação para garantir que todos os elementos da minha estrutura sejam visitados, mas ao mesmo tempo eu quero tratar alguns elementos igualmente e outros. É como @AndreasScheinert colocou. Basicamente, quero correspondência de padrão com um aviso do compilador se a correspondência não for exaustiva (como a correspondência de Scala com as classes de caso).
Johannes Luong
0

Bem, sim, você poderia. Capture os pontos em comum dos muitos elementos de configuração atribuindo funções a eles. Você pode acabar com instanceof, mas não de uma maneira que viole o princípio DRY, mas como uma maneira de contornar a digitação estática do Java.

class ConfigurationElementA implements IConfigurationElement, ICommittable {
}

class ConfigurationElementB implements IConfigurationElement, IVerifiable {
}

class Visitor {
    void accept(IConfigurationElement element) {
        if (element instanceof ICommittable) {
            // ...
        }

        // Note: not a chain of instanceofs.

        if (element instanceof IVerifiable) {
            // ...
        }
    }
}

Em outras palavras, você permite que o visitante aceite elementos de configuração genericamente e atue em grupos de implementações por meio de funções. Observe que você pode ser o mais específico possível modelando seus elementos de configuração de acordo.

Você pode reconhecer aqui o padrão RoleInterface de Martin Fowler .

Mihai Danila
fonte
1
Você basicamente usa interfaces para definir subconjuntos dos IConfigurationElementquais podem ser manipulados especificamente com base na associação ao conjunto. Infelizmente, assim que você distinguir objetos com instanceofseu compilador, não será possível ajudá-lo se você esquecer de atualizar um de seus visitantes. Sua solução tem o benefício de que todos os instanceofoperadores são incorporados em um tipo comum, o que ajuda a encontrá-los procurando pelo tipo.
Johannes Luong
Minha proposta vem de duas observações sobre o seu caso de uso: Primeiro, você deseja evitar a proliferação de acceptassinaturas de método. Para isso, sugeri uma abordagem abrangente, que você pode adaptar para atender às suas necessidades, tornando-se mais ou menos específica em sua IConfigurationElementimplementação. Segundo, você desejava a flexibilidade da instância de, presumivelmente (no primeiro caso), porque você tem características comuns em suas classes de implementação - caso contrário, não há razão para evitar a acceptproliferação no início. Foi aí que sugeri RoleInterfaceque usa de forma instanceofdiferente.
Mihai Danila
(Você ainda pode evitar instanceof: IConfigurationElementImplementar IRoleEnablede fazer com que o visitante visite cada elemento de configuração como um item de função habilitado visitRoleEnabled, e ter visitRoleEnabledem cada classe de implementação uma chamada de volta ao visitante para cada uma das funções implementadas. Mas com isso, estamos começando a avançar selvagem em instanceofquando ele está realmente fazendo muito bem).
Mihai Danila
0

Eu posso pensar em algumas soluções em potencial:

  1. crie um método privado na Visitorimplementação e tenha vários visitmétodos na Visitorimplementação que chamam esse método privado.

  2. se o acima for repetido em muitos lugares, considere a criação de uma classe abstrata que implemente Visitore redirecione um subconjunto de visitimplementações para um protected abstractmétodo comum .

  3. crie várias Visitorinterfaces:

    interface AOrB extends IConfigurationElementVisitor {
        <Result> Result accept(AOrBVisitor<Result> visitor);
    
        interface AOrBVisitor<Result> {
            Result visit(A a);
            Result visit(B b);
        }
    }
    
    interface ThisCouldBeOfTypeB extends IConfigurationElementVisitor {
        <Result> Result accept(ThisCouldBeOfTypeBVisitor<Result> visitor);
    
        interface ThisCouldBeOfTypeBVisitor<Result> {
            Result visit(B b);
            Result visit(ThisCouldBeOfTypeB visitable);
        }
    }
    
    class A implements AOrB, ThisCouldBeOfTypeB {...}
    
    class B implements AOrB, ThisCouldBeOfTypeB {...}
    
    class C implements ThisCouldBeOfTypeB {...}

Eu acho que 3 é o que você está procurando. é 100% polimorfismo estático e gera avisos do compilador se um tipo não for tratado. Mas a única desvantagem que consigo pensar em 3 é que manter as Visitable*implementações pode se tornar complexa se houver muitas Visitable*interfaces diferentes .

Eric
fonte