Estabilidad del visitante frente a instancia de flexibilidad

8

Estoy trabajando en una aplicación GUI que genera un archivo de configuración. Tengo una jerarquía de clases para el modelo de configuración y uso un árbol de objetos de esa jerarquía en varios contextos diferentes. Actualmente, uso el patrón Visitor para evitar contaminar mis clases de modelo con código específico de contexto.

interface IConfigurationElement {
    void acceptVisitor(IConfigurationElementVisitor visitor);
}

En una versión anterior, utilizaba cadenas de instanceofcondiciones en lugar de Visitor. Comparando los dos enfoques, veo los siguientes intercambios.

Visitante

  • Es más fácil y seguro agregar nuevos IConfigurationElement. Simplemente agregue una nueva declaración IConfigurationElementVisitory el compilador genera errores para todas las implementaciones de los visitantes. Con las instanceofcadenas, debe recordar todos los lugares que debe extender con el nuevo elemento de configuración. Básicamente instanceofviola el principio DRY ya que duplica la lógica en varios lugares.
  • El patrón de visitante es más eficiente que una cadena de instanceofcondiciones.

en vez de

  • La gran ventaja de instanceofes su flexibilidad. Por ejemplo, instanceof me permite definir soluciones especiales para diferentes subconjuntos de IConfigurationElementimplementaciones que deben manejarse de manera similar en algunos casos. Por el contrario, Visitor me obliga a implementar un método para cada clase de implementación cada vez.

¿Existe una solución común para este tipo de problema? ¿Puedo adaptar el visitante de alguna manera, para poder proporcionar una solución común para algunos casos?

Johannes Luong
fuente
Recomiendo encarecidamente este tutorial de blog que compara el estilo de visitante diferente (instancia de etc.) y proporciona respuestas muy interesantes, me ayudó, espero que también te
sirva

Respuestas:

1

Puede usar el visitante con la instancia

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);
}

Visitables:

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 clase solo conoce sus interfaces relacionadas, por lo que agregar nuevos visitantes o visitantes requiere cambiar todo en esa categoría (visitante / visitable) (para el visitante, no requiere cambiar nada, para visitar requiere crear una nueva interfaz de visitante, pero nuevamente, no cambio de objetos existentes).

De esta manera, no hay una cadena de instancias de pruebas y el visitante para el subconjunto ni siquiera necesita saber acerca de los tipos fuera de este subconjunto.

La pregunta es qué hacer con la situación en la que A extiende a B (y B también es Visitable), en ese caso, podría agregar super.accept (visitante) en accept (por lo tanto, sería una cadena corta de instancias de s, pero solo como siempre y cuando la jerarquía sea profunda, y no debería ser demasiado profunda para importar, y no es necesario que la escriba por completo de forma manual).

user470365
fuente
Si te entiendo correctamente, propones eliminar IConfigurationElementVisitory simplemente verificar los tipos de visitantes específicos Visitable. Si bien esta es una posible solución, veo algunos inconvenientes. Primero, esto eliminaría la estabilidad de Visitor de la que hablé y segundo, incorporaría el conocimiento sobre los visitantes en mis Visitableimplementaciones, lo que evitar era parte de la razón para usar Visitor en primer lugar.
Johannes Luong
No en Visitable (que debería ser solo una interfaz simple con aceptar (Visor de objetos)), sino en implementaciones de Visitable. Por lo tanto, cada clase verifica solo a su visitante, por lo que solo conoce el hecho de que existe una interfaz para visitarlo. Modificaré mi respuesta para aclarar esto.
user470365
Ok, su enfoque permite tener visitantes que visitan solo subtipos específicos de mi estructura. Esta es una buena idea. Lo que quiero es un poco diferente (probablemente debería actualizar mi descripción). Quiero un tiempo de compilación que garantice que todos los elementos de mi estructura fueron visitados, pero al mismo tiempo quiero tratar algunos elementos por igual y otros de manera diferente. Es como lo dijo @AndreasScheinert. Básicamente, quiero la coincidencia de patrones con una advertencia del compilador si la coincidencia no es exhaustiva (como la coincidencia de Scala con las clases de casos).
Johannes Luong
0

Bueno, si puedes. Capture los puntos en común de los muchos elementos de configuración asignándoles roles. Puede terminar con instanceof, pero no de una manera que viole el principio DRY, sino más bien como una forma de evitar la escritura estática de 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) {
            // ...
        }
    }
}

En otras palabras, permite que el visitante acepte elementos de configuración genéricamente y actúe en grupos de implementaciones a través de roles. Tenga en cuenta que puede ir tan específico como necesite modelando sus elementos de configuración en consecuencia.

Puede reconocer aquí el patrón RoleInterface de Martin Fowler .

Mihai Danila
fuente
1
Básicamente, utiliza interfaces para definir subconjuntos de los IConfigurationElementcuales se pueden manejar específicamente en función de la membresía establecida. Desafortunadamente, tan pronto como distingas los objetos con instanceoftu compilador no podrás ayudarte si olvidas actualizar a uno de tus visitantes. Su solución tiene el beneficio de que todos los instanceofoperadores están integrados en un tipo común, lo que ayuda a encontrarlos buscando el tipo.
Johannes Luong
Mi propuesta proviene de dos observaciones sobre su caso de uso: en primer lugar, desea evitar la proliferación de acceptfirmas de métodos. Para esto, sugerí un enfoque general, uno que puede adaptar para satisfacer sus necesidades, más o menos específico en torno a su IConfigurationElementimplementación. En segundo lugar, querías la flexibilidad de la instancia de, presumiblemente (en el primer caso) porque tienes rasgos comunes en tus clases de implementación; de lo contrario, no hay razón para evitar la acceptproliferación para empezar. Ahí es donde sugerí RoleInterfacecuál usa de manera instanceofdiferente.
Mihai Danila
(Todavía puede evitar instanceof: IConfigurationElementImplemente IRoleEnabledy haga que el visitante visite cada elemento de configuración como un elemento habilitado para el rol visitRoleEnabled, y que visitRoleEnableden cada clase de implementación vuelva a llamar al visitante para cada uno de los roles implementados. Pero con esto comenzamos a ir salvaje instanceofcuando realmente está bien.)
Mihai Danila
0

Se me ocurren algunas soluciones potenciales:

  1. cree un método privado en la Visitorimplementación y tenga varios visitmétodos en la Visitorimplementación, llame a ese método privado.

  2. Si lo anterior se repite en muchos lugares, puede considerar crear una clase abstracta que implemente Visitory redirija un subconjunto de visitimplementaciones a un protected abstractmétodo común .

  3. crear múltiples 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 {...}
    

Creo que 3 es lo que estás buscando. es 100% polimorfismo estático y genera advertencias del compilador si no se maneja un tipo. Pero la única desventaja que puedo pensar en 3 es que mantener las Visitable*implementaciones podría volverse complejo si hay muchas Visitable*interfaces diferentes .

Eric
fuente