El Modo Observador, también conocido como Modo Publicar/Suscripción, fue propuesto por el grupo de cuatro personas (GOF, a saber, Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides) en el "Patrón de diseño de 1994: los conceptos básicos del software reutilizable de objetos reutilizables" (ver páginas 293-313 en el libro para detalles). Aunque este patrón tiene una historia considerable, todavía es ampliamente aplicable a una variedad de escenarios e incluso se ha convertido en una parte integral de la Biblioteca Java estándar. Aunque ya hay muchos artículos sobre los patrones de observadores, todos se centran en la implementación en Java, pero ignoran los diversos problemas encontrados por los desarrolladores cuando usan patrones de observadores en Java.
La intención original de escribir este artículo es llenar este vacío: este artículo presenta principalmente la implementación del patrón del observador mediante el uso de la arquitectura Java8, y explora más a fondo problemas complejos sobre los patrones clásicos sobre esta base, incluidas las clases internas anónimas, las expresiones de Lambda, la seguridad de los hilos y la implementación del observador que no requiere tiempo tradicional. Aunque el contenido de este artículo no es integral, muchos de los problemas complejos involucrados en este modelo no pueden explicarse en un solo artículo. Pero después de leer este artículo, los lectores pueden entender cuál es el patrón del observador, su universalidad en Java y cómo lidiar con algunos problemas comunes al implementar el patrón del observador en Java.
Modo de observador
Según la definición clásica propuesta por GOF, el tema del patrón Observer es:
Define una dependencia de uno a muchos entre objetos. Cuando cambia el estado de un objeto, todos los objetos que dependen de él se notifican y se actualizan automáticamente.
¿Qué significa? En muchas aplicaciones de software, los estados entre objetos son interdependientes. Por ejemplo, si una aplicación se centra en el procesamiento de datos numéricos, estos datos se pueden mostrar a través de tablas o gráficos de la interfaz gráfica de usuario (GUI) o utilizados al mismo tiempo, es decir, cuando se actualizan los datos subyacentes, los componentes de la GUI correspondientes también deben actualizarse. La clave del problema es cómo actualizar los datos subyacentes cuando se actualizan los componentes de la GUI y, al mismo tiempo, minimice el acoplamiento entre los componentes de la GUI y los datos subyacentes.
Una solución simple y no escalable es referirse a los componentes de la GUI de la tabla y la imagen de los objetos que administran estos datos subyacentes, para que los objetos puedan notificar a los componentes de la GUI cuando cambia los datos subyacentes. Obviamente, esta solución simple mostró rápidamente sus deficiencias para aplicaciones complejas que manejan más componentes de la GUI. Por ejemplo, hay 20 componentes de la GUI que dependen de los datos subyacentes, por lo que los objetos que administran datos subyacentes deben mantener referencias a estos 20 componentes. A medida que aumenta el número de objetos que dependen de los datos relacionados, el grado de acoplamiento entre la gestión de datos y los objetos se vuelve difícil de controlar.
Otra mejor solución es permitir que los objetos se registren para obtener permisos para actualizar los datos de interés, que el administrador de datos notificará a esos objetos cuando los datos cambien. En términos de Layman, deje que el objeto de datos de interés le diga al gerente: "Por favor, notifíqueme cuando los datos cambien". Además, estos objetos no solo pueden registrarse para obtener notificaciones de actualización, sino también cancelar el registro para asegurarse de que el administrador de datos ya no notifique el objeto cuando los datos cambian. En la definición original de GOF, el objeto registrado para obtener actualizaciones se llama "observador", el administrador de datos correspondiente se llama "sujeto", los datos que el observador está interesado se llama "estado de destino", el proceso de registro se llama "agregar" y el proceso de deshacer la observación se denomina "separarse". Como se mencionó anteriormente, el modo Observador también se llama Modo Publish-Susscribe. Se puede entender que un cliente se suscribe al observador sobre el objetivo. Cuando se actualiza el estado de destino, el objetivo publica estas actualizaciones al suscriptor (este patrón de diseño se extiende a una arquitectura general, llamada Arquitectura Publish-Susscribe). Estos conceptos pueden estar representados por el siguiente diagrama de clase:
ConcerEteObServer lo usa para recibir cambios de estado de actualización y pasar una referencia a la sujeción de concertides a su constructor. Esto proporciona una referencia a un sujeto específico para un observador específico, a partir del cual se pueden obtener actualizaciones cuando cambia el estado. En pocas palabras, se le dirá al observador específico que actualice el tema y, al mismo tiempo, use las referencias en su constructor para obtener el estado del tema específico, y finalmente almacene estos objetos de estado de búsqueda bajo la propiedad ObserverState del observador específico. Este proceso se muestra en el siguiente diagrama de secuencia:
Profesionalización de modelos clásicos
Aunque el modelo Observador es universal, hay muchos modelos especializados, los más comunes de los cuales son los siguientes dos:
Proporciona un parámetro al objeto de estado, pasado al método de actualización llamado por el observador. En el modo clásico, cuando se notifica al observador que el estado del sujeto ha cambiado, su estado actualizado se obtendrá directamente del sujeto. Esto requiere que el observador guarde una referencia de objeto al estado recuperado. Esto forma una referencia circular, la referencia de ConcreteSubject apunta a su lista de observadores, y la referencia de ConcreteBServer apunta al ConcreteSubect que puede obtener el estado de sujeto. Además de obtener el estado actualizado, no hay conexión entre el observador y el sujeto que registra para escuchar. El observador se preocupa por el objeto de estado, no el sujeto en sí. Es decir, en muchos casos, concreteObServer y concreteSubject están unidos por la fuerza. Por el contrario, cuando ConcreteSubject llama a la función de actualización, el objeto de estado se pasa a ConcreteBServer, y los dos no necesitan estar asociados. La asociación entre concreteobserver y el objeto de estado reduce el grado de dependencia entre el observador y el estado (ver el artículo de Martin Fowler para obtener más diferencias en asociación y dependencia).
Fusionar la clase abstracta del tema y el concreteSubject en una clase de sujeción single. En la mayoría de los casos, el uso de clases abstractas en el tema no mejora la flexibilidad y la escalabilidad del programa, por lo que combinar esta clase abstracta y la clase concreta simplifica el diseño.
Después de combinar estos dos modelos especializados, el diagrama de clase simplificado es el siguiente:
En estos modelos especializados, la estructura de la clase estática se simplifica enormemente y las interacciones entre las clases también se simplifican. El diagrama de secuencia en este momento es el siguiente:
Otra característica del modo de especialización es la eliminación de la variable observación de la variable miembro de ConcreteBServer. A veces, el observador específico no necesita guardar el último estado del sujeto, pero solo necesita monitorear el estado del sujeto cuando se actualiza el estado. Por ejemplo, si el Observador actualiza el valor de la variable de miembro a la salida estándar, puede eliminar el ObserverState, que elimina la asociación entre el ConcreteBServer y la clase de estado.
Reglas de nombres más comunes
El modelo clásico e incluso el modelo profesional mencionado anteriormente usan términos como Adjunto, separar y observador, mientras que muchas implementaciones de Java utilizan diferentes diccionarios, incluidos Registro, Unregister, oyente, etc. Vale la pena mencionar que el estado es un término general para todos los objetos que el oyente necesita para monitorear los cambios. El nombre específico del objeto de estado depende del escenario utilizado en el modo Observador. Por ejemplo, en el modo Observador en la escena en la que el oyente escucha al evento, el oyente registrado recibirá una notificación cuando ocurra el evento. El objeto de estado en este momento es el evento, es decir, si el evento ha ocurrido.
En aplicaciones reales, el nombramiento de objetivos rara vez incluye un sujeto. Por ejemplo, cree una aplicación sobre un zoológico, registre varios oyentes para observar la clase del zoológico y recibir notificaciones cuando los nuevos animales ingresan al zoológico. El objetivo en este caso es la clase de zoológico. Para mantener la terminología consistente con el dominio del problema dado, no se utilizará el término "sujeto", lo que significa que la clase del zoológico no se denominará Zoosubject.
El nombre del oyente generalmente es seguido por el sufijo del oyente. Por ejemplo, el oyente mencionado anteriormente para monitorear nuevos animales se nombrará AnimalAddedListener. Del mismo modo, el nombramiento de funciones como Registro, Unregister y Notify a menudo se sufre por sus nombres de oyentes correspondientes. Por ejemplo, el registro, la falta de registro y las funciones de notificación de AnimalAddedListener serán nombrados RegistaniMaladdedListener, UnregeranimaladdedListener y NotifyAnimalAddedListeners. Cabe señalar que se usa el nombre de la función de notificación, porque la función de notificación maneja múltiples oyentes en lugar de un solo oyente.
Este método de nombres parecerá largo, y generalmente un sujeto registrará múltiples tipos de oyentes. Por ejemplo, en el ejemplo del zoológico mencionado anteriormente, en el zoológico, además de registrar nuevos oyentes para monitorear animales, también necesita registrar un oyente a los animales para reducir los oyentes. En este momento, habrá dos funciones de registro: (RegisterAnImalAdDedListener y RegisterAnImalRemovedListener. De esta manera, el tipo de oyente se usa como un calificador para indicar el tipo de observador. Otra solución es crear una función de registro de registro y luego sobre la solución, pero la solución 1 puede saber más convenientemente qué oyente está escuchando. Overloading es un enfoque relativamente niche.
Otra sintaxis idiomática es usar en el prefijo en lugar de la actualización, por ejemplo, la función de actualización se llama onanimaladded en lugar de actualateMaladded. Esta situación es más común cuando el oyente recibe notificaciones para una secuencia, como agregar un animal a la lista, pero rara vez se usa para actualizar los datos separados, como el nombre del animal.
A continuación, este artículo utilizará las reglas simbólicas de Java. Aunque las reglas simbólicas no cambiarán el diseño real y la implementación del sistema, es un principio de desarrollo importante utilizar los términos con los que otros desarrolladores están familiarizados, por lo que debe estar familiarizado con las reglas simbólicas del patrón Observer en Java descritas anteriormente. El concepto anterior se explicará a continuación utilizando un ejemplo simple en el entorno Java 8.
Un ejemplo simple
También es el ejemplo del zoológico mencionado anteriormente. Uso de la interfaz API de Java8 para implementar un sistema simple, explicando los principios básicos del patrón de observador. El problema se describe como:
Cree un zoológico del sistema, que permite a los usuarios escuchar y deshacer el estado de agregar un nuevo animal de objeto y crear un oyente específico, responsable de generar el nombre del nuevo animal.
Según el aprendizaje anterior del patrón de observador, sabemos que para implementar dicha aplicación, necesitamos crear 4 clases, específicamente:
Clase del zoológico: es decir, el tema en el patrón, que es responsable de almacenar a todos los animales en el zoológico y notificar a todos los oyentes registrados cuando se unen nuevos animales.
Clase animal: representa un objeto animal.
AnimalAddedListener Clase: es decir, interfaz de observador.
PrintNameanImalAddedListener: la clase de observador específica es responsable de generar el nombre del animal recién agregado.
Primero creamos una clase de animales, que es un objeto Java simple que contiene variables de nombre de nombre, constructores, getters y métodos setter. El código es el siguiente:
public class Animal {private String name; public animal (nombre de cadena) {this.name = name;} public String getName () {return this.name;} public void setName (name de cadena) {this.name = name;}}Use esta clase para representar objetos animales, y luego puede crear la interfaz AnimalAddedListener:
interfaz pública animaladdedlistener {public void onanimaladded (animal animal);}Las dos primeras clases son muy simples, por lo que no las presentaré en detalle. A continuación, cree la clase de zoológico:
Public Class Zoo {Private List <Men animal> Animals = new ArrayList <> (); Private List <AnimalDedListener> oyentes = new ArrayList <> (); public void addanimal (animal animal) {// Agregue el animal a la lista de animales.animals.add (animal); // notifica la lista de los oyentes registrados. void registeraniMaladdedListener (animaladdedlistener oyente) {// Agregue el oyente a la lista de oyentes registrados que.listeners.add (oyente);} public void no regeranimaladdedlistener (animaladdedlistener oyente) {// Eliminar el oyente de la lista de la lista de la lista de la lista de la lista de la lista registrada oyentes this.listeners.remove (oyente);} Void protegido notifyaniMaladdedListeners (animal animal) {// notifica a cada oyente en la lista de oyentes registrados oyentes.Esta analogía es compleja que las dos anteriores. Contiene dos listas, una se usa para almacenar todos los animales en el zoológico y el otro se usa para almacenar a todos los oyentes. Dado que los objetos almacenados en animales y colecciones de oyentes son simples, este artículo eligió ArrayList para el almacenamiento. La estructura de datos específica del oyente almacenado depende del problema. Por ejemplo, para el problema del zoológico aquí, si el oyente tiene prioridad, debe elegir otra estructura de datos o reescribir el algoritmo de registro del oyente.
La implementación del registro y la eliminación es un método de delegado simple: cada oyente se agrega o elimina de la lista de escucha del oyente como parámetro. La implementación de la función de notificación está ligeramente apagada del formato estándar del patrón de observación. Incluye el parámetro de entrada: el animal recién agregado, para que la función de notificación pueda pasar la referencia de animales recién agregada al oyente. Use la función foreach de la API de transmisión para atravesar los oyentes y ejecutar la función theonanimaladded en cada oyente.
En la función addanimal, el objeto animal recientemente agregado y el oyente se agregan a la lista correspondiente. Si no se tiene en cuenta la complejidad del proceso de notificación, esta lógica debe incluirse en un método de llamada conveniente. Solo necesita pasar en una referencia al objeto animal recién agregado. Esta es la razón por la cual la implementación lógica del oyente de notificaciones se encapsula en la función NotifyAnimalAddedListener, que también se menciona en la implementación de Addanimal.
Además de los problemas lógicos de las funciones de notificación, es necesario enfatizar el tema controvertido sobre la visibilidad de las funciones de notificación. En el modelo de observador clásico, como dijo GOF en la página 301 de los patrones de diseño del libro, la función de notificación es pública, pero aunque se usa en el patrón clásico, esto no significa que debe ser público. La selección de visibilidad debe basarse en la aplicación. Por ejemplo, en el ejemplo del zoológico de este artículo, la función de notificación es de tipo protegida y no requiere que cada objeto inicie una notificación de un observador registrado. Solo necesita asegurarse de que el objeto pueda heredar la función de la clase principal. Por supuesto, este no es exactamente el caso. Es necesario averiguar qué clases pueden activar la función de notificación y luego determinar la visibilidad de la función.
A continuación, debe implementar la clase PrintNameanImalAdDedListener. Esta clase utiliza el método System.out.println para generar el nombre del nuevo animal. El código específico es el siguiente:
clase pública PrintNneMeanImalAdDedListener implementa animalAddedListener {@OverridePublic Void updateAnImaladded (animal animal) {// imprime el nombre del recién agregado animalystem.out.println ("agregó un nuevo animal con el nombre '" + animal.getName () + "'");}}Finalmente, necesitamos implementar la función principal que impulsa la aplicación:
public class Main {public static void main (string [] args) {// Crea el zoológico para almacenar animaleszoo zoo = new zoo (); // Registre a un oyente a ser notificado cuando se agrega un animal. Animal ("tigre"));}}La función principal simplemente crea un objeto de zoológico, registra un oyente que genera el nombre del animal y crea un nuevo objeto animal para activar el oyente registrado. La salida final es:
Se agregó un nuevo animal con el nombre 'Tiger'
Oyente agregado
Las ventajas del modo Observador se muestran completamente cuando el oyente se restablece y se agrega al sujeto. Por ejemplo, si desea agregar un oyente que calcule el número total de animales en un zoológico, solo necesita crear una clase de oyente específica y registrarlo con la clase de zoológico sin ninguna modificación a la clase de zoológico. Agregar el código de conteador CountinganiMaladDedDedDedEnder de contar es el siguiente:
La clase pública CountinganiMalAddedListener implementa animalAddedListener {private static int animalAdDedCount = 0; @OverridePublic void updateAniMaladded (animal animal) {// incrementa el número de animalessystem.out.println ("Total Animals agregó:" + AnimalDedDedCount);}}La función principal modificada es la siguiente:
public class Main {public static void main (string [] args) {// Crear el zoológico para almacenar animaleszoo zoo = new zoo (); // Los oyentes de registro para ser notificados cuando se agrega un animal. Agregue un animal notifique a los oyentes registrados.El resultado de la salida es:
Se agregó un nuevo animal con el nombre de animales totales 'tigre' agregado: 1 Se agregó un nuevo animal con el nombre de animales totales 'león' agregado: 2 agregó un nuevo animal con nombre 'oso' animales totales agregados: 3
El usuario puede crear cualquier oyente si solo modifica el código de registro del oyente. Esta escalabilidad se debe principalmente a que el sujeto está asociado con la interfaz del observador, en lugar de estar directamente asociado con el servidor concretero. Mientras la interfaz no se modifique, no es necesario modificar el sujeto de la interfaz.
Clases internas anónimas, funciones lambda y registro del oyente
Una mejora importante en Java 8 es la adición de características funcionales, como la adición de funciones Lambda. Antes de introducir la función Lambda, Java proporcionó funciones similares a través de clases internas anónimas, que todavía se usan en muchas aplicaciones existentes. En el modo Observador, se puede crear un nuevo oyente en cualquier momento sin crear una clase de observador específica. Por ejemplo, la clase PrintNameanImalAdDedListener se puede implementar en la función principal con la clase interna anónima. El código de implementación específico es el siguiente:
clase pública Main {public static void main (string [] args) {// Crea el zoológico para almacenar animaleszoo zoo = new zoo (); // Los oyentes registrados para ser notificados cuando se agrega un animal. Animalsystem.out.println ("Se agregó un nuevo animal con el nombre '" + animal.getName () + "'");}}); // Agregue un animal notificar a los oyentes registrados.Del mismo modo, las funciones Lambda también se pueden usar para completar tales tareas:
public class Main {public static void main (string [] args) {// Cree el zoológico para almacenar animaleszoo zoo = new zoo (); // Los oyentes de registro para ser notificados cuando se agrega un animal. oyenteszoo.addanimal (nuevo animal ("tigre"));}}Cabe señalar que la función Lambda solo es adecuada para situaciones en las que solo hay una función en la interfaz del oyente. Aunque este requisito parece estricto, muchos oyentes son en realidad funciones individuales, como AnimalAddedListener en el ejemplo. Si la interfaz tiene múltiples funciones, puede optar por usar clases internas anónimas.
Existe tal problema con el registro implícito del oyente creado: dado que el objeto se crea dentro del alcance de la llamada de registro, es imposible almacenar una referencia a un oyente específico. Esto significa que los oyentes registrados a través de funciones Lambda o clases internas anónimas no pueden ser revocadas porque las funciones de revocación requieren una referencia al oyente registrado. Una manera fácil de resolver este problema es devolver una referencia al oyente registrado en la función RegistraNeMalAdDedListener. De esta manera, puede desanimar al oyente creado con funciones Lambda o clases internas anónimas. El código de método mejorado es el siguiente:
Public AnimalAdDedListener RegisterAniMaladdedListener (AnimalDedListener Listener) {// Agregue el oyente a la lista de oyentes registrados que.listeners.add (oyente); return oyente;}El código del cliente para la interacción de la función rediseñada es el siguiente:
public class Main {public static void main (string [] args) {// Crea el zoológico para almacenar animaleszoo zoo = new zoo (); // Los oyentes registrados para ser notificados cuando un animal se agregue un animaladdedlistener oyente = zoo.regeraniMaladdedlistener ((animal) -> system.println ("agregó un nuevo animal con name '" "" + animal. "'")); // Agregue un animal notificar al oyente registrado.addanimal (nuevo animal ("tigre"))); // no registrar el oyente.unregisteraniMaladdedlistener (oyente); // Agregar otro animal, que no imprimirá el nombre, ya que el oyente // ha sido no registrado.La salida del resultado en este momento solo se agrega un nuevo animal con el nombre 'Tiger', porque el oyente ha sido cancelado antes de agregar el segundo animal:
Se agregó un nuevo animal con el nombre 'Tiger'
Si se adopta una solución más compleja, la función de registro también puede devolver la clase de receptor para que se llame al oyente no registrado, por ejemplo:
clase pública AnimalAdDedListenErReceipt {oyente privado final de animaladdedlistener; public animaladdedListenErReceipt (animalAddedListener oyer) {this.listener = oyente;} public Final AnimalAdDedListener GetListener () {return this.listener;}}El recibo se utilizará como el valor de devolución de la función de registro y se cancelan los parámetros de entrada de la función de registro. En este momento, la implementación del zoológico es la siguiente:
clase pública ZoousingReceipt {// ... Atributos y constructor existentes ... public AnimalAdDedListenErReceipt RegistroaniMaladDedListener (animaladdedlistener oyente) {// Agregue el oyente a la lista de oyentes registrados. UnregisteraniMaladdedListener (animalAddedListenErReceipt Recepción) {// Elimine el oyente de la lista de los oyentes registrados que.listeners.remove (recibe.getListener ());} // ... Método de notificación existente ...}El mecanismo de implementación de recepción descrito anteriormente permite el almacenamiento de información para llamar al oyente al revocar, es decir, si el algoritmo de registro de revocación depende del estado del oyente cuando el sujeto registre el oyente, este estado se guardará. Si el registro de revocación solo requiere una referencia al oyente registrado anterior, la tecnología de recepción parecerá problemática y no se recomienda.
Además de los oyentes específicos particularmente complejos, la forma más común de registrar oyentes es a través de funciones Lambda o a través de clases internas anónimas. Por supuesto, hay excepciones, es decir, la clase que contiene sujeto implementa la interfaz del observador y registra a un oyente que llama al objetivo de referencia. El caso como se muestra en el siguiente código:
clase pública Zoocontainer implementa animalAddedListener {private zoo zoo zoo = new zoo (); public Zoocontainer () {// Registre este objeto como un oyente de oyente {System.out.println ("Se agregó el animal con el nombre '" + animal.getName () + "'");} public static void main (string [] args) {// Cree el zoológico contenedorzoocontainer zoocontainer = new zoocontainer (); // Agregar un animal notificador de animal Animal ("tigre"));}}Este enfoque solo es adecuado para casos simples y el código no parece lo suficientemente profesional, y sigue siendo muy popular entre los desarrolladores modernos de Java, por lo que es necesario comprender cómo funciona este ejemplo. Debido a que Zoocontainer implementa la interfaz AnimalAddedListener, entonces una instancia (u objeto) de zoocontainer puede registrarse como un animalAddedListener. En la clase Zoocontainer, esta referencia representa una instancia del objeto actual, a saber, zoocontainer, y puede usarse como un animalAddedListener.
En general, no todas las clases de contenedores son necesarias para implementar tales funciones, y la clase de contenedores que implementa la interfaz del oyente solo puede llamar a la función de registro del sujeto, sino simplemente pasar la referencia a la función de registro como el objeto del oyente. En los siguientes capítulos, se introducirán preguntas frecuentes y soluciones para entornos multiproceso.
OneApm le proporciona soluciones de rendimiento de la aplicación Java de extremo a extremo. Apoyamos todos los marcos de Java y los servidores de aplicaciones comunes para ayudarlo a descubrir rápidamente los cuellos de botella del sistema y localizar las causas fundamentales de las anormalidades. Despliegue en niveles y experiencia en minuto al instante, el monitoreo de Java nunca ha sido más fácil. Para leer más artículos técnicos, visite el blog de tecnología oficial de ONEAPM.
El contenido anterior introduce el contenido relevante de usar Java 8 para implementar el modo Observador (Parte 1). El siguiente artículo presenta el método de usar Java 8 para implementar el modo Observador (Parte 2). ¡Los amigos interesados continuarán aprendiendo, esperando que sea útil para todos!