strategy source pattern patron observer making explicacion oop design-patterns visitor

oop - source - visitor pattern c#



¿Alternativa al patrón de visitante? (7)

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. Usaré un ejemplo de Shape (¡lo siento!):

  1. Usted tiene una jerarquía de objetos que implementan la interfaz IShape
  2. Dispone de varias operaciones globales que se realizarán en todos los objetos de la jerarquía, por ejemplo, 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 se realizará en todas las formas, cada clase derivada de IShape debe cambiarse
  4. Implementar 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 en el 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 lees sobre el patrón del visitante dicen que el punto 5 es básicamente el criterio principal para que el patrón funcione y estoy totalmente de acuerdo. Si la cantidad de clases derivadas de IShape es fija, entonces este puede ser un enfoque bastante elegante.

Entonces, el problema es 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.

Entonces, la pregunta es si alguien se encuentra con enfoques alternativos para manejar esta situación.


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


Es posible que desee echar un vistazo al patrón de Estrategia . 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 está en esta discusión relacionada:

¿Debería un objeto escribirse a sí mismo en un archivo, o debería actuar sobre él otro objeto para realizar E / S?


Existe el "Patrón de visitante con incumplimiento", en el que se realiza el patrón de visitante como siempre, pero luego se define una clase abstracta que implementa su clase IShapeVisitor delegando todo en 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 de 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 agrega una nueva subclase IShape , entonces, arregla la clase abstracta para delegar en su método visitDefault , y cada visitante que especificó un comportamiento predeterminado obtiene ese comportamiento para 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 visitantes que especifiquen su comportamiento en el nivel de especificidad que desee.

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. (Ver también el segundo panel de esta caricatura )


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 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 tiene que escribir implementaciones concretas para trabajar en nuevas versiones de ''forma'', pero debido a que está completamente separada de la interfaz de forma, puede actualizar esta solución sin romper la interfaz original y el software que interactúa con ella. Actúa como una especie de andamiaje en torno a las implementaciones de IShape.


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

Cuando convertimos nuestro software por primera vez (¡se lanzó por primera vez en 1985!) A un objeto orientado a 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 de vista pasiva y una interfaz bien definida entre las capas de software.

Nuestro software está estructurado básicamente así

  • Los formularios que implementan varios interfaz de formulario. 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 del formulario.
  • La capa de IU ejecutará comandos que implementarán todos la interfaz de comando
  • El objeto UI 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.

Entonces 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 XY. 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 lo siguiente

Tenemos programas de formas que producen rutas de corte a través de los parámetros ingresados. 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 ruta de corte del diseño de la forma, que no siempre es lo mismo. 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 es que tenemos clientes a quienes 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 caen 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 una nueva funcionalidad agregamos otro objeto de comando, buscamos un menú, un teclado corto o un botón en la barra de herramientas en la pantalla de UI correcta y configuramos el objeto de 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 Comando MirrorPath se está asociando con un elemento UI 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.

Descubrirá 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, puede 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.

Agrupación de acciones en objetos de Comando ayuda con esta meta mucho mejor que una devoción servil a los ideales de encapsulación. Todo lo que se necesita hacer para Duplicar una ruta está incluido en el Objeto de comando Mirror Path.


Si está usando 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.


Si tiene n IShape 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 (usando 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 una nueva forma IShape o cuando agrega una nueva operación, no hay forma de IShape .

Si está buscando que cada operación implemente una función predeterminada de IShape , eso resolvería su problema, como en la respuesta de Daniel Martin: https://.com/a/986034/1969638 , aunque probablemente usaría la sobrecarga:

interface IVisitor { void visit(IShape shape); void visit(Rectangle shape); void visit(Circle shape); } interface IShape { //... void accept(IVisitor visitor); }