Patrón de diseño | De patrón de visitante a coincidencia de patrones

Prefacio


En el campo del desarrollo de software, los problemas que encontramos cada vez pueden ser diferentes: algunos están relacionados con el comercio electrónico, otros están relacionados con la estructura de datos subyacente y algunos pueden centrarse en la optimización del rendimiento. Sin embargo, nuestro enfoque para resolver problemas a nivel de código tiene ciertos puntos en común. ¿Alguien ha resumido estos puntos en común?

Por supuesto que sí. En 1994, Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides publicaron conjuntamente un libro de gran importancia en la industria: Design Patterns: Elements of Reusable Object-Oriented Software. Este libro acerca a la gente al campo del desarrollo. Se han realizado una serie de abstracciones sobre los puntos en común de varios temas y se han formado 23 patrones de diseño muy clásicos. Muchos problemas se pueden resumir en uno o más de estos 23 patrones de diseño. Debido a que los patrones de diseño son muy versátiles, también se han convertido en un lenguaje universal utilizado por los desarrolladores, y el código abstraído en patrones de diseño es más fácil de entender y mantener.

En general, los patrones de diseño se dividen en tres categorías:


1. Patrones de creación: Patrones de diseño relacionados con la creación y reutilización de objetos
2. Patrones estructurales: Patrones de diseño relacionados con la combinación y construcción de objetos
3. Patrones de comportamiento: Patrones de diseño relacionados con el comportamiento entre objetos

El patrón de diseño que se describe en este artículo es el patrón de visitante, que es una especie de patrón de comportamiento, que se utiliza para resolver el problema de cómo combinar y expandir objetos con comportamientos similares. Más específicamente, este artículo presenta los escenarios de uso, las ventajas y las desventajas de Visitor Pattern y la tecnología Double Dispatch relacionados con Visitor Pattern. Y al final del artículo, se explica cómo usar Pattern Matching, que se lanzó recientemente en Java 14, para resolver los problemas resueltos por el Patrón de Visitante anterior.

problema


Suponiendo que hay un programa de mapa, hay muchos nodos en el mapa, como edificio (Edificio), fábrica (Fábrica), escuela (Escuela), como se muestra a continuación:

interface Node {
    String getName();
    String getDescription();
    // 其余的方法这里忽略......
}


class Building implements Node {
    ...
}


class Factory implements Node {
    ...
}


class School implements Node {
    ...
}

Aquí viene un nuevo requisito: necesitas agregar la función de dibujar Node. Si lo piensas, es muy simple. Agregaremos un método draw () a Node, y luego el resto de las clases de implementación implementarán este método por separado. Pero hay un problema al hacer esto. Agregamos el método draw () esta vez. ¿Qué hay de agregar un método de exportación la próxima vez? Además, la interfaz debe modificarse nuevamente. Como componentes de conexión de puente, las interfaces deben ser lo más estables posible y no deben cambiarse con frecuencia. Por lo tanto, desea poder hacer que la escalabilidad de la interfaz sea lo más alta posible y maximizar el alcance funcional de la interfaz sin cambiarla con frecuencia. Después de pesar un poco, se le ocurrió la siguiente solución.

Solución inicial


Definimos una nueva clase DrawService y escribimos toda la lógica de dibujo en ella. El código es el siguiente:

public class DrawService {
    public void draw(Building building) {
        System.out.println("draw building");
    }
    public void draw(Factory factory) {
        System.out.println("draw factory");
    }
    public void draw(School school) {
        System.out.println("draw school");
    }
    public void draw(Node node) {
        System.out.println("draw node");
    }
}

Este es el diagrama de clases:

Crees que el problema está resuelto ahora, así que vas a ir a casa del trabajo después de una pequeña prueba:

public class App {
    private void draw(Node node) {
        DrawService drawService = new DrawService();
        drawService.draw(node);
    }


    public static void main(String[] args) {
        App app = new App();
        app.draw(new Factory());
    }
}

Haga clic para ejecutar, salida:

draw node

¿Cómo va esto? Echa un vistazo más de cerca a su código nuevamente: "Pasé un objeto Factory, debería generar draw factory". En serio, fuiste a verificar cierta información y luego encontraste el motivo.

explica la razón


Para comprender la razón, primero comprendemos los dos modos de vinculación de tipo variable del editor.

★ Enlace   dinámico / tardío

Echemos un vistazo a este código.

class NodeService {
    public String getName(Node node) {
        return node.getName();
    }
}

Cuando el programa ejecuta NodeService :: getName, debe determinar el tipo de parámetro Node, ya sea Factory, School o Building, para poder llamar al método getName de la clase de implementación correspondiente. ¿Puede el programa obtener esta información durante la fase de compilación? Evidentemente no, porque el tipo de Nodo puede variar según el entorno operativo, e incluso puede ser transmitido desde otro sistema, es imposible que obtengamos esta información durante la etapa de compilación. Lo que el programa puede hacer es iniciarlo primero, y cuando se ejecuta en el método getName, mirar el tipo de Nodo y luego llamar a la implementación getName () del tipo correspondiente para obtener el resultado. Decidir qué método llamar en tiempo de ejecución (no en tiempo de compilación) se llama enlace dinámico / tardío.

★ Enlace   estático / temprano

Veamos otro fragmento de código

public void drawNode(Node node) {
    DrawService drawService = new DrawService();
    drawService.draw(node);
}

Cuando corremos a drawService.draw (nodo), ¿el compilador conoce el tipo de nodo? Debe ser conocido en tiempo de ejecución, entonces, ¿por qué pasamos una Fábrica, pero generamos un nodo de dibujo en lugar de una fábrica de dibujo? Podemos pensar en este problema desde el punto de vista del programa. Solo hay 4 métodos de dibujo en DrawService, y los tipos de parámetros son Fábrica, Edificio, Escuela y Nodo. ​​¿Qué pasa si la persona que llama pasa en una Ciudad? Después de todo, la persona que llama puede implementar una clase City para pasar. ¿Qué método debería llamar el programa en este caso? No tenemos un método draw (City), para evitar que esto suceda, el programa elige directamente usar el método DrawService :: draw (Node) durante la fase de compilación. No importa qué implementación pase el llamador, usaremos el método DrawService :: draw (Node) para garantizar el funcionamiento seguro del programa. Decidir qué método llamar en tiempo de compilación (no en tiempo de ejecución) se llama enlace estático / anticipado. Esto también explica por qué generamos un nodo de dibujo.

La solución definitiva


Resulta que esto se debe a que el compilador no conoce el tipo de variable, en este caso podemos decirle al compilador de qué tipo es. Se puede hacer esto? Por supuesto, esto se puede hacer, verificamos el tipo de variable de antemano.

if (node instanceof Building) {
    Building building = (Building) node;
    drawService.draw(building);
} else if (node instanceof Factory) {
    Factory factory = (Factory) node;
    drawService.draw(factory);
} else if (node instanceof School) {
    School school = (School) node;
    drawService.draw(school);
} else {
    drawService.draw(node);
}

Este código es factible, pero es muy engorroso de escribir. Necesitamos dejar que la persona que llama determine el tipo de nodo y elija el método a llamar. ¿Existe una mejor solución? Sí, ese es el Patrón de Visitante. El Patrón de Visitante usa un método llamado Doble Despacho, que puede transferir el trabajo de enrutamiento del llamador a la clase de implementación respectiva, de modo que el cliente no necesita escribir esta tediosa lógica de juicio. Primero veamos cómo se ve el código implementado.

interface Visitor {
    void visit(Node node);
    void visit(Factory factory);
    void visit(Building building);
    void visit(School school);
}


class DrawVisitor implements Visitor {


    @Override
    public void visit(Node node) {
        System.out.println("draw node");
    }


    @Override
    public void visit(Factory factory) {
        System.out.println("draw factory");
    }


    @Override
    public void visit(Building building) {
        System.out.println("draw building");
    }


    @Override
    public void visit(School school) {
        System.out.println("draw school");
    }
}


interface Node {
    ...
    void accpet(Visitor v);
}


class Factory implements Node {
    ...


    @Override
    public void accept(Visitor v) {
        /**
         * 调用方知道visit的参数就是Factory类型的,并且知道Visitor::visit(Factory)方法确实存在,
         * 因此会直接调用Visitor::visit(Factory)方法
         */
        v.visit(this);
    }
}


class Building implements Node {
    ...


    @Override
    public void accept(Visitor v) {
        /**
         * 调用方知道visit的参数就是Building类型的,并且知道Visitor::visit(Building)方法确实存在,
         * 因此会直接调用Visitor::visit(Building)方法
         */
        v.visit(this);
    }
}


class School implements Node {
    ...


    @Override
    public void accept(Visitor v) {
        /**
         * 调用方知道visit的参数就是School类型的,并且知道Visitor::visit(School)方法确实存在,
         * 因此会直接调用Visitor::visit(School)方法
         */
        v.visit(this);
    }
}

La persona que llama puede usarlo así

Visitor drawVisitor = new DrawVisitor();
Factory factory = new Factory();
factory.accept(drawVisitor);

Se puede ver que Visitor Pattern implementa elegantemente nuestra instancia if anterior, de modo que el código de la persona que llama es mucho más limpio, el diagrama de clases general es el siguiente

¿Por qué se llama Doble Despacho?


Después de comprender cómo el Patrón de Visitante resuelve este problema, algunos estudiantes pueden tener curiosidad ¿Por qué la tecnología utilizada por el Patrón de Visitante se llama Doble Despacho? ¿Qué es exactamente el envío doble? Antes de comprender el envío doble, comprendamos primero lo que se llama envío único

★   Envío único

Elija diferentes métodos de llamada de acuerdo con las diferentes implementaciones de clases de tiempo de ejecución, que se llama Envío único, como

String name = node.getName();

¿Estamos llamando a Factory :: getName, School :: getName o Building :: getName? Esto depende principalmente de la clase de implementación del nodo, que es Despacho único: una capa de enrutamiento

★   Envío doble

Revise el código del patrón de visitante que acabamos de ver

node.accept(drawVisitor);

Hay dos capas de enrutamiento:

  • Elija el método de implementación específico de accept (Factory :: accept, School :: accept o Building :: accept)

  • Seleccione el método específico de visita (en este ejemplo, solo hay una DrawVisit :: visita)

Después de hacer dos rutas, se ejecuta la lógica correspondiente, que se denomina Doble Despacho.



Ventajas del patrón de visitantes


1. El patrón de visitante puede aumentar la escalabilidad de la interfaz tanto como sea posible sin cambiar la interfaz con frecuencia (solo es necesario cambiarlo una vez: agregar un método de aceptación)    

Aún con el ejemplo de dibujo anterior, suponga que ahora tenemos un nuevo requisito y necesitamos agregar la función de mostrar información de nodo. Por supuesto, el método tradicional es agregar un nuevo método showDetails () en Node, pero ahora no necesitamos cambiar la interfaz, solo necesitamos agregar un nuevo Visitor.

class ShowDetailsVisitor implements Visitor {


    @Override
    public void visit(Node node) {
        System.out.println("node details");
    }


    @Override
    public void visit(Factory factory) {
        System.out.println("factory details");
    }


    @Override
    public void visit(Building building) {
        System.out.println("building details");
    }


    @Override
    public void visit(School school) {
        System.out.println("school details");
    }
}


// 调用方这么使用
Visitor showDetailsVisitor = new ShowDetailsVisitor();
Factory factory = new Factory();
factory.accept(showDetailsVisitor); // factory details

En este ejemplo, podemos ver un escenario de uso típico de Visitor Pattern: es muy adecuado para su uso en escenarios donde los métodos de interfaz deben agregarse con frecuencia. Por ejemplo, ahora tenemos 4 clases A, B, C, D, tres métodos x, y, z, método de dibujo horizontal, clase de dibujo vertical, podemos obtener la siguiente imagen:

               x      y      z
    A       A::x   A::y   A::z
    B       B::x   B::y   B::z
    C       C::x   C::y   C::z

En circunstancias normales, nuestra tabla se expande verticalmente, es decir, estamos acostumbrados a agregar clases de implementación en lugar de métodos de implementación. El patrón de visitantes resulta adecuado para otro escenario: la expansión horizontal. Necesitamos agregar métodos de interfaz con frecuencia, en lugar de agregar clases de implementación. Visitor Pattern nos permite lograr este objetivo sin modificar frecuentemente la interfaz.

2. Visitor Pattern puede hacer que múltiples clases de implementación compartan una lógica

Dado que todos los métodos de implementación están escritos en una clase (como DrawVisitor), podemos hacer que cada tipo (como Factory / Building / School) use fácilmente la misma lógica en lugar de escribir esta lógica repetidamente en cada implementación de interfaz Clase.

Desventajas del patrón de visitantes


  • Visitor Pattern rompe la encapsulación del modelo de dominio

En circunstancias normales, escribiremos la lógica de Factory en la clase Factory, pero el Patrón de visitante requiere que muevamos parte de la lógica de Factory (como dibujar) a otra clase (DrawVisitor). La lógica de un modelo de dominio se divide en dos En este lugar, esto trae inconvenientes para la comprensión y el mantenimiento del modelo de dominio.

  • El patrón de visitante hasta cierto punto provocó la realización del acoplamiento lógico de clases

Todos los métodos (dibujo) de la clase de implementación (Fábrica / Escuela / Edificio) están todos escritos en una clase (DrawVisitor), que es un acoplamiento lógico hasta cierto punto y no conduce al mantenimiento del código.

  • Visitor Pattern hace que la relación entre clases sea complicada y no fácil de entender

Como muestra el nombre Double Dispatch, necesitamos dos despachos para llamar con éxito a la lógica correspondiente: el primer paso es llamar al método accpet y el segundo es llamar al método visit. La relación de llamada se vuelve más complicada y el código subyacente El encargado del mantenimiento puede estropear fácilmente el código.

La coincidencia de patrones


Aquí hay otro episodio. Java 14 introdujo la función de coincidencia de patrones, aunque esta función ha existido en el campo de Scala / Haskel durante muchos años, muchos estudiantes aún no saben qué es porque se acaba de introducir Java. Por lo tanto, antes de explicar la relación entre la coincidencia de patrones y el patrón del visitante, vamos a presentar brevemente qué es la coincidencia de patrones. ¿Recuerdas que escribimos este código?

if (node instanceof Building) {
    Building building = (Building) building;
    drawService.draw(building);
} else if (node instanceof Factory) {
    Factory factory = (Factory) factory;
    drawService.draw(factory);
} else if (node instanceof School) {
    School school = (School) school;
    drawService.draw(school);
} else {
    drawService.draw(node);
}

Con Pattern Matching, podemos simplificar este código:

if (node instanceof Building building) {
    drawService.draw(building);
} else if (node instanceof Factory factory) {
    drawService.draw(factory);
} else if (node instanceof School school) {
    drawService.draw(school);
} else {
    drawService.draw(node);
}

Sin embargo, la coincidencia de patrones de Java sigue siendo un poco engorrosa, mientras que la de Scala puede ser mejor:

node match {
  case node: Factory => drawService.draw(node)
  case node: Building => drawService.draw(node)
  case node: School => drawService.draw(node)
  case _ => drawService.draw(node)
}

Debido a que es relativamente conciso, muchas personas abogan por la coincidencia de patrones como sustituto del patrón de visitantes. Personalmente, creo que Pattern Matching parece mucho más simple. Mucha gente piensa que Pattern Matching es la versión avanzada del caso del interruptor. De hecho, no lo es. Para obtener más información, consulte TOUR OF SCALA-PATTERN MATCHING (https://docs.scala-lang.org/tour/pattern-matching.html), sobre Visitor Pattern La relación con Pattern Matching se puede ver en Pattern Matching = Visitor Pattern on Steroids de Scala, este artículo no lo repetirá.

Materiales de referencia:

  • Coincidencia de patrones de Scala = Patrón de visitante con esteroides

    http://andymaleh.blogspot.com/2008/04/scalas-pattern-matching-visitor-pattern.html 

  • ¿Cuándo debo utilizar el patrón de diseño de visitantes?

    http://andymaleh.blogspot.com/2008/04/scalas-pattern-matching-visitor-pattern.html 

  • Patrón de diseño - Patrones de comportamiento - Visitante

    https://refactoring.guru/design-patterns/visitor 

  • Coincidencia de patrones, por ejemplo, en Java 14

    https://refactoring.guru/design-patterns/visitor 

Departamento de Tao Departamento de tecnología-Industria y operación inteligente-Reclutamiento de talentos

Somos el equipo de análisis de datos del banco de trabajo de operaciones de Alibaba. Hay enormes cantidades de datos, motores informáticos de alto rendimiento en tiempo real y escenarios comerciales desafiantes. Desde 618 hasta Double 11, desde Taobao hasta Tmall, desde el análisis de datos hasta la precipitación empresarial, difundiremos la voluntad y la atmósfera de buscar la perfección en todos los rincones del círculo tecnológico. ¡Esperamos que se una con persecución técnica y profundidad técnica!

Puesto de contratación: experto en tecnología Java, ingeniero de datos,
si está interesado, envíe su currículum a [email protected], bienvenido a recoger ~

✿ Más   lecturas

Autor | Yu Haining (Jing Fan)

Editar | Naranja

Producido | Nueva tecnología minorista de Alibaba

Supongo que te gusta

Origin blog.csdn.net/Taobaojishu/article/details/111503210
Recomendado
Clasificación