2009-06-12 24 views
44

Estoy buscando una alternativa al patrón de visitante. Permítanme centrarme en un par de aspectos pertinentes del patrón, omitiendo detalles sin importancia. Voy a utilizar un ejemplo de la forma (lo siento!):¿Alternativa al patrón de visitante?

  1. Usted tiene una jerarquía de objetos que implementan la interfaz IShape
  2. Usted tiene un número de operaciones globales que se van a realizar en todos los objetos de la jerarquía , p.ej Draw, WriteToXml etc ...
  3. Es tentador sumergirse directamente y agregar un método Draw() y WriteToXml() a la interfaz IShape. Esto no es necesariamente algo bueno: cada vez que desee agregar una nueva operación que deba realizarse en todas las formas, cada clase derivada de IShape debe cambiarse
  4. Implementación de un visitante para cada operación, es decir, un visitante de Draw o un visitante de WirteToXml encapsula todo el código para esa operación en una clase. Agregar una nueva operación es una cuestión de crear una nueva clase de visitante que realice la operación en todos los tipos de IShape
  5. Cuando necesite agregar una nueva clase derivada de IShape, esencialmente tendrá el mismo problema que tuvo en 3 - todas las clases de visitantes se deben cambiar para agregar un método para manejar el nuevo tipo derivado de IShape

La mayoría de los lugares donde lee sobre el patrón de visitante indican que el punto 5 es más o menos el criterio principal para que el patrón funcione y yo totalmente de acuerdo. Si la cantidad de clases derivadas de IShape es fija, entonces este puede ser un enfoque bastante elegante.

Por lo tanto, el problema es que cuando se agrega una nueva clase derivada de IShape, cada implementación de visitante necesita agregar un nuevo método para manejar esa clase. Esto es, en el mejor de los casos, desagradable y, en el peor de los casos, imposible y muestra que este patrón en realidad no está diseñado para hacer frente a dichos cambios.

Por lo tanto, la pregunta es ¿alguien se ha llegado a través de enfoques alterativas para manejar esta situación?

+0

Solo una nota aparte, ya que es poco probable que pueda cambiar su idioma: hay idiomas que soportan directamente varias funciones genéricas de despacho. – Svante

+6

Gran pregunta. Solo quería proporcionar un contrapunto. A veces su problema con (5) puede ser algo bueno. Uso el patrón de visitante cuando tengo alguna funcionalidad que debe actualizarse cuando se define un nuevo subtipo IShape. Tengo una interfaz IShapeVisitor que define qué métodos son necesarios. Siempre que esa interfaz se actualice con el nuevo subtipo, mi código no se compila hasta que se actualice la funcionalidad crítica. Para algunas situaciones, esto puede ser muy útil. – oillio

+1

Estoy de acuerdo con @oillio, pero también podría aplicarlo como método abstracto en IShape. Lo que el patrón Visitor le compra en un idioma OO puro es la localidad de la función (frente a la localidad de clase) y, por lo tanto, una separación de preocupaciones.En cualquier caso, use el patrón de visitante que debe romperse explícitamente en tiempo de compilación cuando desee forzar la adición de nuevos tipos para que se revisen con cuidado. –

Respuesta

13

Es posible que desee echar un vistazo a Strategy pattern. Esto todavía le ofrece una separación de preocupaciones al mismo tiempo que puede agregar nuevas funcionalidades sin tener que cambiar cada clase en su jerarquía.

class AbstractShape 
{ 
    IXmlWriter _xmlWriter = null; 
    IShapeDrawer _shapeDrawer = null; 

    public AbstractShape(IXmlWriter xmlWriter, 
       IShapeDrawer drawer) 
    { 
     _xmlWriter = xmlWriter; 
     _shapeDrawer = drawer; 
    } 

    //... 
    public void WriteToXml(IStream stream) 
    { 
     _xmlWriter.Write(this, stream); 

    } 

    public void Draw() 
    { 
     _drawer.Draw(this); 
    } 

    // any operation could easily be injected and executed 
    // on this object at run-time 
    public void Execute(IGeneralStrategy generalOperation) 
    { 
     generalOperation.Execute(this); 
    } 
} 

Más información se encuentra en esta discusión relacionada:

Should an object write itself out to a file, or should another object act on it to perform I/O?

+0

He marcado esto como la respuesta a mi pregunta ya que creo que esto, o alguna variación menor en él, probablemente encaje en lo que quiero hacer. Para cualquier persona cuyos interesados, he añadido una "respuesta" que describe algunos de mis pensamientos sobre el problema – Steg

+0

bien - cambiado de opinión acerca de la cosa respuesta - Voy a tratar de condensarlo en un comentario (siguiente) – Steg

+2

creo que hay aquí hay un conflicto fundamental: si tienes un montón de cosas y un montón de acciones que se pueden realizar sobre estas cosas, entonces agregar algo nuevo significa que debes definir el efecto de todas las acciones en él y viceversa, no hay escapatoria esta. El visitante proporciona una manera muy simple y elegante de agregar nuevas acciones a costa de dificultar la adición de cosas nuevas. Si esta restricción debe relajarse, debe pagar. Esperaba que pudiera haber una solución que tuviera la elegancia y la simplicidad del visitante, pero como sospechaba, no creo que exista ... cont. ... – Steg

13

No es el "patrón de visitantes con defecto", en la que se hace el patrón de visitantes como de costumbre, pero luego definir una clase abstracta que implementa su clase IShapeVisitor delegando todo a un método abstracto con la firma visitDefault(IShape).

Luego, cuando defina un visitante, amplíe esta clase abstracta en lugar de implementar la interfaz directamente. Puede anular los métodos visit * que conoce en ese momento y proporcionar un valor predeterminado sensato. Sin embargo, si realmente no hay forma de descubrir el comportamiento predeterminado sensato con anticipación, debe implementar la interfaz directamente.

Cuando se agrega una nueva subclase IShape, entonces, fijar la clase abstracta para delegar a su método visitDefault, y cada visitante que especifica un comportamiento predeterminado consigue que el comportamiento de la nueva IShape.

Una variación de esto si sus clases IShape caen naturalmente en una jerarquía es hacer que la clase abstracta delegue a través de varios métodos diferentes; por ejemplo, un DefaultAnimalVisitor podría hacer:

public abstract class DefaultAnimalVisitor implements IAnimalVisitor { 
    // The concrete animal classes we have so far: Lion, Tiger, Bear, Snake 
    public void visitLion(Lion l) { visitFeline(l); } 
    public void visitTiger(Tiger t) { visitFeline(t); } 
    public void visitBear(Bear b) { visitMammal(b); } 
    public void visitSnake(Snake s) { visitDefault(s); } 

    // Up the class hierarchy 
    public void visitFeline(Feline f) { visitMammal(f); } 
    public void visitMammal(Mammal m) { visitDefault(m); } 

    public abstract void visitDefault(Animal a); 
} 

Esto le permite definir los visitantes que especifican su comportamiento en cualquier nivel de especificidad que desea.

Desafortunadamente, no hay forma de evitar hacer algo para especificar cómo se comportarán los visitantes con una nueva clase: puede configurar un valor predeterminado de antemano o no puede hacerlo. (Consulte también el segundo panel de this cartoon)

6

Tengo un software CAD/CAM para la máquina de corte de metales. Así que tengo algo de experiencia con estos problemas.

Cuando convertimos por primera vez nuestro software (¡se lanzó por primera vez en 1985!) A un objeto orientado al diseño, hice exactamente lo que no te gusta. Los Objetos e Interfaces tenían Draw, WriteToFile, etc. Descubrir y leer acerca de los Patrones de Diseño a mitad de la conversión ayudaron mucho, pero todavía había muchos malos olores de código.

Eventualmente me di cuenta de que ninguno de estos tipos de operaciones era realmente la preocupación del objeto. Pero más bien los diversos subsistemas que necesitaban para hacer las diversas operaciones. Manejé esto usando lo que ahora se llama un objeto de comando Passive View y una interfaz bien definida entre las capas de software.

Nuestro software está estructurado básicamente así

  • Las formas de aplicación varia forma interfaz. Estas formas son una cosa que pasa eventos de shell a la capa de interfaz de usuario.
  • Capa de interfaz de usuario que recibe eventos y manipula formularios a través de la interfaz de formulario.
  • La capa de interfaz de usuario ejecutará comandos que implementan todos la interfaz de comandos
  • El objeto de la interfaz de usuario tiene interfaces propias con las que el comando puede interactuar.
  • Los comandos obtienen la información que necesitan, la procesan, manipulan el modelo y luego informan a los objetos de la interfaz de usuario que luego hacen todo lo necesario con los formularios.
  • Finalmente, los modelos que contienen los diversos objetos de nuestro sistema. Como programas de formas, rutas de corte, tablas de corte y hojas de metal.

Así que el dibujo se maneja en la capa de interfaz de usuario. Tenemos diferentes programas para diferentes máquinas. Entonces, si bien todos nuestros programas comparten el mismo modelo y reutilizan muchos de los mismos comandos. Manejan cosas como dibujar muy diferente. Por ejemplo, una mesa de corte es diferente para una máquina enrutadora que para una máquina que usa una antorcha de plasma, a pesar de que ambas son esencialmente una mesa plana gigante X-Y. Esto porque al igual que los automóviles, las dos máquinas están construidas de manera diferente, de modo que existe una diferencia visual para el cliente.

En cuanto a las formas lo que hacemos es la siguiente

Tenemos programas de forma que producen trayectorias de corte a través de los parámetros introducidos. La ruta de corte sabe qué programa de forma produjo. Sin embargo, un camino de corte no es una forma. Es solo la información necesaria para dibujar en la pantalla y cortar la forma. Una razón para este diseño es que las rutas de corte se pueden crear sin un programa de formas cuando se importan desde una aplicación externa.

Este diseño nos permite separar el diseño de la trayectoria de corte desde el diseño de la forma, que no siempre son la misma cosa. En su caso, lo único que necesita para empacar es la información necesaria para dibujar la forma.

Cada programa de formas tiene varias vistas que implementan una interfaz IShapeView. A través de la interfaz IShapeView, el programa de forma puede indicarle a la forma de forma genérica que tenemos cómo configurarse para mostrar los parámetros de esa forma. La forma de forma genérica implementa una interfaz IShapeForm y se registra con el Objeto ShapeScreen. El Objeto ShapeScreen se registra con nuestro objeto de aplicación. Las vistas de formas usan cualquier pantalla de formas que se registre a sí misma con la aplicación.

El motivo de las vistas múltiples que tenemos los clientes a los que les gusta ingresar formas de diferentes maneras. Nuestra base de clientes se divide a la mitad entre aquellos a los que les gusta ingresar parámetros de forma en forma de tabla y aquellos a quienes les gusta ingresar con una representación gráfica de la forma que tienen delante. También necesitamos acceder a los parámetros a veces a través de un diálogo mínimo en lugar de nuestra pantalla de entrada de forma completa. De ahí las múltiples vistas.

Los comandos que manipulan las formas se clasifican en una de dos categorías. O manipulan la ruta de corte o manipulan los parámetros de forma. Para manipular los parámetros de forma en general, los devolvemos a la pantalla de entrada de forma o mostramos el diálogo mínimo. Vuelva a calcular la forma y muéstrela en la misma ubicación.

Para la ruta de corte agrupamos cada operación en un objeto de comando separado. Por ejemplo, tenemos objetos de comando

ResizePath RotatePath MovePath SplitPath y así sucesivamente.

Cuando necesitamos agregar nuevas funciones, agregamos otro objeto de comando, buscamos un menú, un teclado corto o un botón en la pantalla de UI correcta y configuramos el objeto UI para ejecutar ese comando.

Por ejemplo

CuttingTableScreen.KeyRoute.Add vbShift+vbKeyF1, New MirrorPath 

o

CuttingTableScreen.Toolbar("Edit Path").AddButton Application.Icons("MirrorPath"),"Mirror Path", New MirrorPath 

En ambos casos el objeto MIRRORPATH Comando se está asociado con un elemento de interfaz de usuario deseado. En el método de ejecución de MirrorPath está todo el código necesario para duplicar la ruta en un eje particular. Es probable que el comando tenga su propio cuadro de diálogo o utilice uno de los elementos de la interfaz de usuario para preguntar al usuario qué eje duplicar. Nada de esto está haciendo un visitante, o agregando un método a la ruta.

Encontrará que se puede manejar mucho combinando acciones en comandos. Sin embargo, advierto que no es una situación en blanco y negro. Todavía encontrará que ciertas cosas funcionan mejor como métodos en el objeto original. En mi experiencia, descubrí que tal vez el 80% de lo que solía hacer en los métodos se podía mover al comando. El último 20% simplemente funciona mejor en el objeto.

Ahora es posible que a algunos no les guste esto porque parece violar las encapsulaciones. Desde el mantenimiento de nuestro software como un sistema orientado a objetos durante la última década, tengo que decir que lo más importante a largo plazo que puede hacer es documentar claramente las interacciones entre las diferentes capas de su software y entre los diferentes objetos.

El agrupamiento de acciones en Objetos de comando ayuda con esta meta mucho mejor que una dedicación servil a los ideales de la encapsulación.Todo lo que se necesita hacer para Duplicar una ruta está incluido en el Objeto de comando Mirror Path.

+0

La solución parece interesante, pero le agradecería que me remitiera al código de ejemplo de dicha solución para comprender mejor el concepto. –

2

Independientemente de la ruta que tome, la implementación de la funcionalidad alternativa que actualmente proporciona el patrón Visitor tendrá que 'saber' algo sobre la implementación concreta de la interfaz en la que está trabajando. Por lo tanto, no se puede olvidar el hecho de que tendrá que escribir la funcionalidad adicional de "visitante" para cada implementación adicional. Dicho esto, lo que está buscando es un enfoque más flexible y estructurado para crear esta funcionalidad.

Debe separar la funcionalidad del visitante de la interfaz de la forma.

Lo que yo propondría es un enfoque creacionista a través de una fábrica abstracta para crear implementaciones de reemplazo para la funcionalidad del visitante.

public interface IShape { 
    // .. common shape interfaces 
} 

// 
// This is an interface of a factory product that performs 'work' on the shape. 
// 
public interface IShapeWorker { 
    void process(IShape shape); 
} 

// 
// This is the abstract factory that caters for all implementations of 
// shape. 
// 
public interface IShapeWorkerFactory { 
    IShapeWorker build(IShape shape); 
    ... 
} 

// 
// In order to assemble a correct worker we need to create 
// and implementation of the factory that links the Class of 
// shape to an IShapeWorker implementation. 
// To do this we implement an abstract class that implements IShapeWorkerFactory 
// 
public AbsractWorkerFactory implements IShapeWorkerFactory { 

    protected Hashtable map_ = null; 

    protected AbstractWorkerFactory() { 
      map_ = new Hashtable(); 
      CreateWorkerMappings(); 
    } 

    protected void AddMapping(Class c, IShapeWorker worker) { 
      map_.put(c, worker); 
    } 

    // 
    // Implement this method to add IShape implementations to IShapeWorker 
    // implementations. 
    // 
    protected abstract void CreateWorkerMappings(); 

    public IShapeWorker build(IShape shape) { 
     return (IShapeWorker)map_.get(shape.getClass()) 
    } 
} 

// 
// An implementation that draws circles on graphics 
// 
public GraphicsCircleWorker implements IShapeWorker { 

    Graphics graphics_ = null; 

    public GraphicsCircleWorker(Graphics g) { 
     graphics_ = g; 
    } 

    public void process(IShape s) { 
     Circle circle = (Circle)s; 
     if(circle != null) { 
      // do something with it. 
      graphics_.doSomething(); 
     } 
    } 

} 

// 
// To replace the previous graphics visitor you create 
// a GraphicsWorkderFactory that implements AbstractShapeFactory 
// Adding mappings for those implementations of IShape that you are interested in. 
// 
public class GraphicsWorkerFactory implements AbstractShapeFactory { 

    Graphics graphics_ = null; 
    public GraphicsWorkerFactory(Graphics g) { 
     graphics_ = g; 
    } 

    protected void CreateWorkerMappings() { 
     AddMapping(Circle.class, new GraphicCircleWorker(graphics_)); 
    } 
} 


// 
// Now in your code you could do the following. 
// 
IShapeWorkerFactory factory = SelectAppropriateFactory(); 

// 
// for each IShape in the heirarchy 
// 
for(IShape shape : shapeTreeFlattened) { 
    IShapeWorker worker = factory.build(shape); 
    if(worker != null) 
     worker.process(shape); 
} 

Todavía significa que usted tiene que escribir implementaciones concretas para trabajar en nuevas versiones de 'forma' sino porque está completamente separada de la interfaz de forma, se puede adaptar esta solución sin romper la interfaz y el software original que interactúa con eso. Actúa como una especie de andamiaje en torno a las implementaciones de IShape.

+0

en AbstractWorkerFactory, ¿aún tiene que hacer instanceof –

1

Si está utilizando Java: Sí, se llama instanceof. La gente está demasiado asustada para usarlo. Comparado con el patrón de visitante, generalmente es más rápido, más directo y no está plagado por el punto n. ° 5.

+0

más rápido? Verifique [esto] (http://alexshabanov.com/2011/12/03/instanceof-vs-visitor/). – ntohl

+0

@ntohl En las pruebas que he hecho (en Java 8, tenga en cuenta que la prueba utilizada Java 6) instanciaof fue más rápido, por lo que supongo que la velocidad de la velocidad relativa de los dos debe variar en función de los detalles sutiles. – Andy

1

Si tiene n IShape sym operaciones que se comportan de forma diferente para cada forma, entonces necesita n * m funciones individuales. Poner todos estos en la misma clase me parece una idea terrible, dándote algún tipo de objeto de Dios. Por lo tanto, deben agruparse por IShape, poniendo m funciones, una para cada operación, en la interfaz IShape, o agrupadas por operación (mediante el patrón de visitante), poniendo n funciones, una para cada IShape en cada operación/visitante clase.

O bien tiene que actualizar varias clases cuando agrega un nuevo IShape o cuando agrega una nueva operación, no hay forma de evitarlo.


Si usted está buscando para cada operación para implementar una función predeterminada IShape, entonces eso sería resolver su problema, como en la respuesta de Daniel Martin: https://stackoverflow.com/a/986034/1969638, a pesar de que probablemente utilice la sobrecarga:

interface IVisitor 
{ 
    void visit(IShape shape); 
    void visit(Rectangle shape); 
    void visit(Circle shape); 
} 

interface IShape 
{ 
    //... 
    void accept(IVisitor visitor); 
} 
3

El patrón de diseño del visitante es una solución, no una solución al problema. La respuesta corta sería pattern matching.

Cuestiones relacionadas