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:
Especialización de modelos clásicos <Br /> Aunque el modelo observador es universal, también hay muchos modelos especializados, los más comunes de los cuales son los siguientes dos:
1. Proporcione un parámetro al objeto de estado y pase 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).
2. 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 nomenclatura más comunes <Br /> Los modos clásicos e incluso el modo profesional mencionado anteriormente usan los términos, como adjuntar, separar y observar, mientras que muchas implementaciones de Java usan diferentes diccionarios, incluidos los registros, no registrados, el 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:
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 {Nombre de cadena privada; Public Animal (nombre de cadena) {this.name = name; } public String getName () {return this.name; } public void setName (nombre 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:
zoológico de clase pública {Lista privada <Menimy> Animals = New ArrayList <> (); Lista privada <AnimalDedListener> oyentes = new ArrayList <> (); public void addanimal (animal animal) {// Agregue el animal a la lista de animales this.animals.add (animal); // notifica a la lista de oyentes registrados esto. NotifyaniMaladdedListeners (animal); } public void RegisterAniMalAdDedListener (AnimalAddedListener Listener) {// Agregue el oyente a la lista de oyentes registrados this.listeners.add (oyente); } public void no regheraniMaladdedListener (animalAddedListener oyente) {// elimina el oyente de la lista de los oyentes registrados this.listeners.remove (oyente); } NotifyAniMalAddedListeners (animal animal) {// notifica a cada uno de los oyentes en la lista de oyentes registrados oyentes this.listeners.foreach (oyente -> oyente.updateaniMaladded (animal)); }}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:
Public Class PrintNneMeanImalAdDedListener implementa AnimalAddedListener {@Override public void updateEnimaladded (animal animal) {// imprime el nombre del sistema animal recién agregado.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) {// Cree el zoológico para almacenar animales zoo zoo = new zoo (); // Registre a un oyente que se le notifique cuando se agrega un animal zoológico. // Agregar un animal notificar al zoológico de oyentes registrados. Addanimal (nuevo 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 animalSaddedCount = 0; @Override public void updateaniMaladded (animal animal) {// incrementa el número de animales animales de puestos ++; // Imprima el número de animales System.out.println ("Total Animales agregó:" + AnimalsAddedCount); }}La función principal modificada es la siguiente:
Public Class Main {public static void main (string [] args) {// Cree el zoológico para almacenar animales zoo zoo = new zoo (); // registra a los oyentes que se notificarán cuando se agrega un animal zoológico. zoo. // Agregar un animal notificar al zoológico de oyentes registrados. Addanimal (nuevo animal ("tigre")); zoo.addanimal (nuevo animal ("león")); zoo.addanimal (nuevo animal ("oso")); }}El resultado de la salida es:
Se agregó un nuevo animal con el nombre 'Tiger'total Animals agregado: 1aded a un nuevo animal con el nombre' Animales de Lion'total agregado: 2aded un nuevo animal con nombre 'Bear'total Animals agregado: 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:
Public Class Main {public static void main (string [] args) {// Cree el zoológico para almacenar animales zoo zoo = new zoo (); // registra a los oyentes que se notificarán cuando se agrega un animal zoológico. RegisteraniMaladdedListener (new AnimalDDedListener () {@Override public void updateEniMaladded (animal animal) {// imprima el nombre del sistema de animales recién agregado.println ("agregó un nuevo animal con nombre '" + animal.getName () + "");});});});}); // Agregar un animal notificar al zoológico de oyentes registrados. Addanimal (nuevo animal ("tigre")); }}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 animales zoo zoo = new zoo (); // registra a los oyentes que se notificarán cuando se agrega un animal zoológico. // Agregar un animal notificar al zoológico de oyentes registrados. 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 this.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) {// Cree el zoológico para almacenar animales zoo zoo = new zoo (); // registra a los oyentes que se notificarán cuando un animal se agrega animalAddedListener oyente = zoo.registeranimaladdedlistener ((animal) -> system.out.println ("Se agregó un nuevo animal con el nombre '" + animal.getName () + "'")); // Agregar un animal notificar al zoológico de oyentes registrados. Addanimal (nuevo animal ("tigre")); // no registrar el oyente zoo.UnregisteraniMaladdedListener (oyente); // Agregar otro animal, que no imprima el nombre, ya que el oyente // ha sido previamente no registrado zoo.addanimal (nuevo animal ("león")); }}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 Listener) {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 (AnimalDedDedListener Listener) {// Agregue el oyente a la lista de oyentes registrados esto.listeners.add (escucha); devolver nuevo animaladdedlistenReceipt (oyente); } public void no regheraniMaladdedlistener (animalAddedListenErReceipt Recepción) {// Elimine el oyente de la lista de los oyentes registrados this.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 = new zoo (); public Zoocontainer () {// Registre este objeto como oyente this.zoo.registerAnimaladdedListener (this); } public zoo getzoo () {return this.zoo; } @Override public void updateAnImaladded (animal animal) {system.out.println ("Se agregó animal con el nombre '" + animal.getName () + "'"); } public static void main (string [] args) {// crea el contenedor de zoológico zoocontainer zoocontainer = new Zoocontainer (); // Agregar un animal notifique al oyente notificado interno Zoocontainer.getzoo (). Addanimal (nuevo 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.
Implementación de la seguridad del hilo <Br /> El capítulo anterior presenta la implementación del patrón de observador en el entorno Java moderno. Aunque es simple pero completo, esta implementación ignora un problema clave: la seguridad de los subprocesos. La mayoría de las aplicaciones de Java abiertas son multiprocesos, y el modo Observador se usa principalmente en sistemas múltiples o asincrónicos. Por ejemplo, si un servicio externo actualiza su base de datos, la aplicación también recibirá un mensaje de manera asincrónica y luego notificará al componente interno para actualizar en modo Observer, en lugar de registrarse directamente y escuchar el servicio externo.
La seguridad de los subprocesos en el modo Observador se centra principalmente en el cuerpo del modo, porque es probable que ocurran conflictos de subprocesos al modificar la colección de oyentes registrados. Por ejemplo, un hilo intenta agregar un nuevo oyente, mientras que el otro hilo intenta agregar un nuevo objeto animal, que desencadena notificaciones a todos los oyentes registrados. Dado el orden de secuencia, el primer hilo puede o no haber completado el registro del nuevo oyente antes de que el oyente registrado reciba notificación del animal agregado. Este es un caso clásico de competencia de recursos de hilos, y es este fenómeno el que les dice a los desarrolladores que necesitan un mecanismo para garantizar la seguridad de los hilos.
La solución más fácil a este problema es: todas las operaciones que acceden o modifican la lista de oyentes de registro deben seguir el mecanismo de sincronización de Java, como:
AnimalAdDedDedDedListener public sincronizado público RegisterAniMaladDedListener (AnimalAdDedListener Listener) {/*...*/} public sincronizado void no registeraniMaladdedListener (animalAdlistener oyente) {/*...*/} público sincronizado sincronizado NotifyAnimaladdedLeners (animal animal) {/* ...De esta manera, al mismo tiempo, solo un hilo puede modificar o acceder a la lista de oyentes registrados, que puede evitar con éxito problemas de competencia de recursos, pero surgen nuevos problemas, y tales restricciones son demasiado estrictas (para obtener más información sobre palabras clave sincronizadas y modelos de concurrencia de Java, consulte la página web oficial). A través de la sincronización del método, el acceso concurrente a la lista de oyentes se puede observar en todo momento. Registrarse y revocar al oyente es una operación de escritura para la lista de oyentes, mientras que notificar al oyente para acceder a la lista de oyentes es una operación de solo lectura. Dado que el acceso a través de la notificación es una operación de lectura, se pueden realizar múltiples operaciones de notificación simultáneamente.
Por lo tanto, siempre que no haya registro o revocación del oyente, siempre que el registro no esté registrado, siempre que cualquier número de notificaciones concurrentes se pueda ejecutar simultáneamente sin desencadenar una competencia de recursos para la lista de oyentes registrados. Por supuesto, la competencia de recursos en otras situaciones ha existido durante mucho tiempo. Para resolver este problema, el bloqueo de recursos para ReadWriteLock está diseñado para administrar las operaciones de lectura y escritura por separado. El código de implementación de ThreadSafezoo de Thread Safe de la clase Zoo es el siguiente:
public class ThreadSafezoo {Private ReadWriteReLock ReadWriteLock = new ReentRantReadWriteLock (); Bloqueo final protegido Readlock = ReadWriteLock.ReadLock (); Bloqueo final protegido WriteLock = ReadWriteLock.WriteLock (); Lista privada <Menimy> Animals = New ArrayList <> (); Lista privada <AnimalDedListener> oyentes = new ArrayList <> (); public void addanimal (animal animal) {// Agregue el animal a la lista de animales this.animals.add (animal); // notificar a la lista de oyentes registrados th.notifyaniMaladdedListeners (animal); } public animalAddedListener RegisterAniMaladdedListener (AnimalDedListener Listener) {// Bloquea la lista de oyentes para escribir esto.writeLock.lock (); Pruebe {// Agregue el oyente a la lista de oyentes registrados this.listeners.add (oyente); } finalmente {// desbloquear el escritor bloquear this.writeLock.unlock (); } Devuelve el oyente; } public void no registeraniMaladdedListener (animaladdedlistener oyente) {// bloquea la lista de oyentes para escribir este.writeLock.lock (); Pruebe {// Elimine el oyente de la lista de los oyentes registrados this.listeners.remove (oyente); } finalmente {// desbloquear el escritor bloquear this.writeLock.unlock (); }} public void notifyaniMaladdedListeners (animal animal) {// bloquea la lista de oyentes para leer esto.readlock.lock (); Pruebe {// notifique a cada uno de los oyentes en la lista de oyentes registrados this.listeners.forach (oyente -> oyente.updateaniMaladded (animal)); } finalmente {// desbloquear el lector bloquear this.readlock.unlock (); }}}A través de dicha implementación, la implementación del sujeto puede garantizar la seguridad de los subprocesos y múltiples hilos pueden emitir notificaciones al mismo tiempo. Pero a pesar de esto, todavía hay dos problemas de competencia de recursos que no se pueden ignorar:
Acceso concurrente a cada oyente. Múltiples hilos pueden notificar al oyente que se necesitan nuevos animales, lo que significa que un oyente puede ser llamado por múltiples hilos al mismo tiempo.
Acceso concurrente a la lista de animales. Múltiples hilos pueden agregar objetos a la lista de animales al mismo tiempo. Si el orden de las notificaciones tiene un impacto, puede conducir a una competencia de recursos, lo que requiere un mecanismo de procesamiento de operación concurrente para evitar este problema. Si la lista de oyentes registrados recibe notificación para agregar animal2 y luego recibe notificación para agregar animal1, se producirá una competencia de recursos. Sin embargo, si la adición de Animal1 y Animal2 se realiza mediante diferentes hilos, también es posible completar la adición de Animal1 antes de Animal2. Específicamente, Thread 1 agrega Animal1 antes de notificar al oyente y bloquea el módulo, Thread 2 agrega Animal2 y notifica al oyente, y luego Thread 1 notifica al oyente que Animal1 ha sido agregado. Aunque la competencia de recursos puede ignorarse cuando no se considera el orden de secuencia, el problema es real.
Acceso concurrente a los oyentes
并发访问监听器可以通过保证监听器的线程安全来实现。秉承着类的“责任自负”精神,监听器有“义务”确保自身的线程安全。例如,对于前面计数的监听器,多线程的递增或递减动物数量可能导致线程安全问题,要避免这一问题,动物数的计算必须是原子操作(原子变量或方法同步),具体解决代码如下:
public class ThreadSafeCountingAnimalAddedListener implements AnimalAddedListener { private static AtomicLong animalsAddedCount = new AtomicLong(0); @Override public void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount.incrementAndGet(); // Print the number of animals System.out.println("Total animals added: " + animalsAddedCount); }}方法同步解决方案代码如下:
public class CountingAnimalAddedListener implements AnimalAddedListener { private static int animalsAddedCount = 0; @Override public synchronized void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount++; // Print the number of animals System.out.println("Total animals added: " + animalsAddedCount); }}要强调的是监听器应该保证自身的线程安全,subject需要理解监听器的内部逻辑,而不是简单确保对监听器的访问和修改的线程安全。否则,如果多个subject共用同一个监听器,那每个subject类都要重写一遍线程安全的代码,显然这样的代码不够简洁,因此需要在监听器类内实现线程安全。
监听器的有序通知当要求监听器有序执行时,读写锁就不能满足需求了,而需要引入一个新的机制,可以保证notify函数的调用顺序和animal添加到zoo的顺序一致。有人尝试过用方法同步来实现,然而根据Oracle文档中的方法同步介绍,可知方法同步并不提供操作执行的顺序管理。它只是保证原子操作,也就是说操作不会被打断,并不能保证先来先执行(FIFO)的线程顺序。ReentrantReadWriteLock可以实现这样的执行顺序,代码如下:
public class OrderedThreadSafeZoo { private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); protected final Lock readLock = readWriteLock.readLock(); protected final Lock writeLock = readWriteLock.writeLock(); private List<Animal> animals = new ArrayList<>(); private List<AnimalAddedListener> listeners = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyAnimalAddedListeners(animal); } public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Add the listener to the list of registered listeners this.listeners.add(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } return listener; } public void unregisterAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } } public void notifyAnimalAddedListeners (Animal animal) { // Lock the list of listeners for reading this.readLock.lock(); try { // Notify each of the listeners in the list of registered listeners this.listeners.forEach(listener -> listener.updateAnimalAdded(animal)); } finally { // Unlock the reader lock this.readLock.unlock(); }}}这样的实现方式,register, unregister和notify函数将按照先进先出(FIFO)的顺序获得读写锁权限。例如,线程1注册一个监听器,线程2在开始执行注册操作后试图通知已注册的监听器,线程3在线程2等待只读锁的时候也试图通知已注册的监听器,采用fair-ordering方式,线程1先完成注册操作,然后线程2可以通知监听器,最后线程3通知监听器。这样保证了action的执行顺序和开始顺序一致。
如果采用方法同步,虽然线程2先排队等待占用资源,线程3仍可能比线程2先获得资源锁,而且不能保证线程2比线程3先通知监听器。问题的关键所在:fair-ordering方式可以保证线程按照申请资源的顺序执行。读写锁的顺序机制很复杂,应参照ReentrantReadWriteLock的官方文档以确保锁的逻辑足够解决问题。
截止目前实现了线程安全,在接下来的章节中将介绍提取主题的逻辑并将其mixin类封装为可重复代码单元的方式优缺点。
主题逻辑封装到Mixin类<br />把上述的观察者模式设计实现封装到目标的mixin类中很具吸引力。通常来说,观察者模式中的观察者包含已注册的监听器的集合;负责注册新的监听器的register函数;负责撤销注册的unregister函数和负责通知监听器的notify函数。对于上述的动物园的例子,zoo类除动物列表是问题所需外,其他所有操作都是为了实现主题的逻辑。
Mixin类的案例如下所示,需要说明的是为使代码更为简洁,此处去掉关于线程安全的代码:
public abstract class ObservableSubjectMixin<ListenerType> { private List<ListenerType> listeners = new ArrayList<>(); public ListenerType registerListener (ListenerType listener) { // Add the listener to the list of registered listeners this.listeners.add(listener); return listener; } public void unregisterAnimalAddedListener (ListenerType listener) { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } public void notifyListeners (Consumer<? super ListenerType> algorithm) { // Execute some function on each of the listeners this.listeners.forEach(algorithm); }}正因为没有提供正在注册的监听器类型的接口信息,不能直接通知某个特定的监听器,所以正需要保证通知功能的通用性,允许客户端添加一些功能,如接受泛型参数类型的参数匹配,以适用于每个监听器,具体实现代码如下:
public class ZooUsingMixin extends ObservableSubjectMixin<AnimalAddedListener> { private List<Animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.updateAnimalAdded(animal)); }}Mixin类技术的最大优势是把观察者模式的Subject封装到一个可重复调用的类中,而不是在每个subject类中都重复写这些逻辑。此外,这一方法使得zoo类的实现更为简洁,只需要存储动物信息,而不用再考虑如何存储和通知监听器。
然而,使用mixin类并非只有优点。比如,如果要存储多个类型的监听器怎么办?例如,还需要存储监听器类型AnimalRemovedListener。mixin类是抽象类,Java中不能同时继承多个抽象类,而且mixin类不能改用接口实现,这是因为接口不包含state,而观察者模式中state需要用来保存已经注册的监听器列表。
One solution is to create a listener type ZooListener that will be notified when animals increase and decrease. El código se ve así:
public interface ZooListener { public void onAnimalAdded (Animal animal); public void onAnimalRemoved (Animal animal);}这样就可以使用该接口实现利用一个监听器类型对zoo状态各种变化的监听了:
public class ZooUsingMixin extends ObservableSubjectMixin<ZooListener> { private List<Animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.onAnimalAdded(animal)); } public void removeAnimal (Animal) animal) { // Remove the animal from the list of animals this.animals.remove(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.onAnimalRemoved(animal)); }}将多个监听器类型合并到一个监听器接口中确实解决了上面提到的问题,但仍旧存在不足之处,接下来的章节会详细讨论。
Multi-Method监听器和适配器
在上述方法,监听器的接口中实现的包含太多函数,接口就过于冗长,例如,Swing MouseListener就包含5个必要的函数。尽管可能只会用到其中一个,但是只要用到鼠标点击事件就必须要添加这5个函数,更多可能是用空函数体来实现剩下的函数,这无疑会给代码带来不必要的混乱。
其中一种解决方案是创建适配器(概念来自GoF提出的适配器模式),适配器中以抽象函数的形式实现监听器接口的操作,供具体监听器类继承。这样一来,具体监听器类就可以选择其需要的函数,对adapter不需要的函数采用默认操作即可。例如上面例子中的ZooListener类,创建ZooAdapter(Adapter的命名规则与监听器一致,只需要把类名中的Listener改为Adapter即可),代码如下:
public class ZooAdapter implements ZooListener { @Override public void onAnimalAdded (Animal animal) {} @Override public void onAnimalRemoved (Animal animal) {}}乍一看,这个适配器类微不足道,然而它所带来的便利却是不可小觑的。比如对于下面的具体类,只需选择对其实现有用的函数即可:
public class NamePrinterZooAdapter extends ZooAdapter { @Override public void onAnimalAdded (Animal animal) { // Print the name of the animal that was added System.out.println("Added animal named " + animal.getName()); }}有两种替代方案同样可以实现适配器类的功能:一是使用默认函数;二是把监听器接口和适配器类合并到一个具体类中。默认函数是Java8新提出的,在接口中允许开发者提供默认(防御)的实现方法。
Java库的这一更新主要是方便开发者在不改变老版本代码的情况下,实现程序扩展,因此应该慎用这个方法。部分开发者多次使用后,会感觉这样写的代码不够专业,而又有开发者认为这是Java8的特色,不管怎样,需要明白这个技术提出的初衷是什么,再结合具体问题决定是否要用。使用默认函数实现的ZooListener接口代码如下示:
public interface ZooListener { default public void onAnimalAdded (Animal animal) {} default public void onAnimalRemoved (Animal animal) {}}通过使用默认函数,实现该接口的具体类,无需在接口中实现全部函数,而是选择性实现所需函数。虽然这是接口膨胀问题一个较为简洁的解决方案,开发者在使用时还应多加注意。
第二种方案是简化观察者模式,省略了监听器接口,而是用具体类实现监听器的功能。比如ZooListener接口就变成了下面这样:
public class ZooListener { public void onAnimalAdded (Animal animal) {} public void onAnimalRemoved (Animal animal) {}}这一方案简化了观察者模式的层次结构,但它并非适用于所有情况,因为如果把监听器接口合并到具体类中,具体监听器就不可以实现多个监听接口了。例如,如果AnimalAddedListener和AnimalRemovedListener接口写在同一个具体类中,那么单独一个具体监听器就不可以同时实现这两个接口了。此外,监听器接口的意图比具体类更显而易见,很显然前者就是为其他类提供接口,但后者就并非那么明显了。
如果没有合适的文档说明,开发者并不会知道已经有一个类扮演着接口的角色,实现了其对应的所有函数。此外,类名不包含adapter,因为类并不适配于某一个接口,因此类名并没有特别暗示此意图。综上所述,特定问题需要选择特定的方法,并没有哪个方法是万能的。
在开始下一章前,需要特别提一下,适配器在观察模式中很常见,尤其是在老版本的Java代码中。Swing API正是以适配器为基础实现的,正如很多老应用在Java5和Java6中的观察者模式中所使用的那样。zoo案例中的监听器或许并不需要适配器,但需要了解适配器提出的目的以及其应用,因为我们可以在现有的代码中对其进行使用。下面的章节,将会介绍时间复杂的监听器,该类监听器可能会执行耗时的运算或进行异步调用,不能立即给出返回值。
Complex & Blocking监听器关于观察者模式的一个假设是:执行一个函数时,一系列监听器会被调用,但假定这一过程对调用者而言是完全透明的。例如,客户端代码在Zoo中添加animal时,在返回添加成功之前,并不知道会调用一系列监听器。如果监听器的执行需要时间较长(其时间受监听器的数量、每个监听器执行时间影响),那么客户端代码将会感知这一简单增加动物操作的时间副作用。
本文不能面面俱到的讨论这个话题,下面几条是开发者调用复杂的监听器时应该注意的事项:
监听器启动新线程。新线程启动后,在新线程中执行监听器逻辑的同时,返回监听器函数的处理结果,并运行其他监听器执行。
Subject启动新线程。与传统的线性迭代已注册的监听器列表不同,Subject的notify函数重启一个新的线程,然后在新线程中迭代监听器列表。这样使得notify函数在执行其他监听器操作的同时可以输出其返回值。需要注意的是需要一个线程安全机制来确保监听器列表不会进行并发修改。
队列化监听器调用并采用一组线程执行监听功能。将监听器操作封装在一些函数中并队列化这些函数,而非简单的迭代调用监听器列表。这些监听器存储到队列中后,线程就可以从队列中弹出单个元素并执行其监听逻辑。这类似于生产者-消费者问题,notify过程产生可执行函数队列,然后线程依次从队列中取出并执行这些函数,函数需要存储被创建的时间而非执行的时间供监听器函数调用。例如,监听器被调用时创建的函数,那么该函数就需要存储该时间点,这一功能类似于Java中的如下操作:
public class
如何使用Java8 实现观察者模式?相信通过这篇文章大家都有了大概的了解了吧!