En el artículo anterior, introduje el método de usar Java8 para implementar el patrón de observador (Parte 1). Este artículo continúa introduciendo el conocimiento relevante del patrón de observador Java8. El contenido específico es el siguiente:
Implementación segura de hilo
El capítulo anterior presenta la implementación del patrón de observador en un 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 (); Private List <imonmon> Animals = New ArrayList <> (); Private List <AnimalDedListener> oyentes = new ArrayList <> (); public void Addanimal (animal animal) {// Agregue el animal a la lista de animales. oyentes this.notifyaniMalAdDedListeners (animal);} public animalAdDedListener RegisterAniMaladDedListener (animalAdDedListener oyente) {// bloquea la lista de oyentes para escribir this.writeLock (); intente {// Agregar el escucha a la lista de oyentes registrados. escritor LockThis.WriteLock.unlock ();} return Listener;} public void unregisteraniMaladDedListener (animalAddedListener oyente) {// bloquea la lista de oyentes para escribir this.writeLock.lock (); try {// elimina el oyente del listado de los oyentes registrados. LOCKTHIS.WriteLock.unlock ();}} public void notifyAniMaladDedListeners (animal animal) {// bloquea la lista de oyentes para lectura this.readlock.lock (); try {// notifique a cada uno de los oyentes en la lista de oyentes registrados. Desbloquee el lector LockThis.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
Los oyentes de acceso concurrente se pueden implementar asegurando la seguridad de los hilos de los oyentes. Adheriendo al espíritu de "responsabilidad propia" de la clase, el oyente tiene la "obligación" de garantizar la seguridad de su propio hilo. Por ejemplo, para el oyente contado anteriormente, aumentar o disminuir el número de animales en múltiples hilos puede conducir a problemas de seguridad de los hilos. Para evitar este problema, el cálculo de los números de animales debe ser operaciones atómicas (variables atómicas o sincronización del método). El código de solución específico es el siguiente:
clase pública ThreadSafeCountingAniMalAdDedListener implementa animalAddedListener {private static atomiclong animalDedDedCount = new AtomiCLong (0);@overridePublic Void UpdateAnImalAdded (animal animal) {// Incrementa el número de animalesystem.out.Println ("Total Animal Animales agregó:" Animales AddedCountCountCountCount);}El código de solución de sincronización del método es el siguiente:
La clase pública CountinganiMalAddedListener implementa animalAddedListener {private static int animalSaddedCount = 0; @Overridepublic sincronizado void updateaniMaladded (animal animal) {// Incrementa el número de animales sints.println ("Total de animales agregados:" + animales addedCount);}}}Debe enfatizarse que el oyente debe garantizar la seguridad de su propio hilo. El sujeto debe comprender la lógica interna del oyente, en lugar de simplemente garantizar la seguridad de los subprocesos para acceder y modificar al oyente. De lo contrario, si múltiples sujetos comparten el mismo oyente, cada clase de asignatura tiene que reescribir el código seguro de hilo. Obviamente, dicho código no es lo suficientemente conciso, por lo que debe implementarse en la clase del oyente.
Notificaciones ordenadas de oyentes
Cuando se requiere que el oyente se ejecute de manera ordenada, el bloqueo de lectura y escritura no puede satisfacer las necesidades, y se debe introducir un nuevo mecanismo para garantizar que el orden de llamadas de la función notificar sea consistente con el orden en que se agrega el animal al zoológico. Algunas personas han tratado de implementarlo utilizando la sincronización del método, pero de acuerdo con la introducción de la sincronización del método en la documentación de Oracle, se puede ver que la sincronización del método no proporciona la gestión de pedidos de la ejecución de la operación. Solo garantiza que las operaciones atómicas no se interrumpan, y no garantiza el orden de subprocesos de la primera ejecución (FIFO). ReentRantReadWriteLock puede implementar dicha orden de ejecución, el código es el siguiente:
Clase pública OrdenedThreadSafezoo {Private Final ReadWriteLock ReadWriteLock = new ReEntRantReadWriteLock (verdadero); Bloqueo final protegido Readlock = ReadWriteLock.ReadLock (); Bloqueo final protegido WriteLock = ReadWriteLock.WriteLock (); Private List <imonmon> Animals = New ArrayList <> (); Private List <AnimalDedListener> oyentes = new ArrayList <> (); public void Addanimal (animal animal) {// Agregue el animal a la lista de animales. oyentes this.notifyaniMalAdDedListeners (animal);} public animalAdDedListener RegisterAniMaladDedListener (animalAdDedListener oyente) {// bloquea la lista de oyentes para escribir this.writeLock (); intente {// Agregar el escucha a la lista de oyentes registrados. escritor LockThis.WriteLock.unlock ();} return Listener;} public void unregisteraniMaladDedListener (animalAddedListener oyente) {// bloquea la lista de oyentes para escribir this.writeLock.lock (); try {// elimina el oyente del listado de los oyentes registrados. LOCKTHIS.WriteLock.unlock ();}} public void notifyAniMaladDedListeners (animal animal) {// bloquea la lista de oyentes para lectura this.readlock.lock (); try {// notifique a cada uno de los oyentes en la lista de oyentes registrados. Desbloquee el lector LockThis.Readlock.unlock ();}}}De esta manera, las funciones de registro, no registrados y notificados obtendrán permisos de bloqueo de lectura y escritura en el orden de la primera en primera salida (FIFO). Por ejemplo, Thread 1 registra a un oyente, Thread 2 intenta notificar al oyente registrado después de comenzar la operación de registro, Thread 3 intenta notificar al oyente registrado cuando Thread 2 está esperando el bloqueo de solo lectura, adoptando el método de pedido justo, el hilo 1 completa primero la operación de registro, luego el hilo 2 puede notificar al oyente y finalmente el hilo 3 notifica el oyente. Esto asegura que la orden de ejecución y la orden de inicio de la acción sean consistentes.
Si se adopta la sincronización del método, aunque el hilo 2 hace cola primero para ocupar recursos, Thread 3 aún puede obtener el bloqueo de recursos antes de Thread 2, y no se puede garantizar que el hilo 2 notifique primero al oyente que el hilo 3. La clave del problema es: el método de orden justo puede garantizar que los hilos ejecuten en el orden en que se aplican los recursos. El mecanismo de orden de los bloqueos de lectura y escritura es muy complicado. Debe consultar la documentación oficial de ReentRantReadWriteLock para garantizar que la lógica del bloqueo sea suficiente para resolver el problema.
La seguridad de los subprocesos se ha implementado hasta ahora, y las ventajas y desventajas de extraer la lógica del tema y encapsular su clase Mixin en unidades de código repetibles se introducirán en los siguientes capítulos.
Lógica del tema encapsulado en la clase Mixin
Es muy atractivo encapsular la implementación del diseño del patrón del observador mencionado anteriormente en la clase de mezcla de destino. En términos generales, los observadores en modo Observador contienen una colección de oyentes registrados; registrar funciones responsables de registrar nuevos oyentes; Funciones de no registro responsables de revocar las funciones registradas no registradas y notificar a las funciones responsables de notificar a los oyentes. Para el ejemplo anterior del zoológico, todas las demás operaciones de la clase del zoológico, excepto que se requiere la lista de animales para el problema, es implementar la lógica del sujeto.
El caso de la clase Mixin se muestra a continuación. Cabe señalar que para hacer que el código sea más conciso, el código sobre la seguridad del subproceso se elimina aquí:
Public Abstract Class ObservableSubjectMixin <IndingerType> {Lista privada <Indingype> oyentes = new ArrayList <> (); public KurcherType RegisterListener (ListenerType Listener) {// Agregue la oyente a la lista de oyentes registrados this.listeners.add (oyente); return oyente; oyente de la lista de los oyentes registrados this.listeners.remove (oyente);} public void NotifyListeners (Consumer <? Super Listenertype> Algorithm) {// Ejecutar alguna función en cada uno de los oyentes this.listeners.foreach (algorithm);}}Debido a que no se proporciona la información de la interfaz del tipo de oyente registrado, no se puede notificar directamente a un oyente específico, por lo que es necesario garantizar que la universalidad de la función de notificación y permitir que el cliente agregue algunas funciones, como aceptar la coincidencia de parámetros de los tipos de parámetros genéricos para que sea aplicable a cada oyente. El código de implementación específico es el siguiente:
Clase pública ZoousingMixin extiende ObservablesUscjectMixin <MeniMeDDedListener> {Lista privada <animal> animales = new ArrayList <> (); public void addanimal (animal animal) {// Agregar el animal a la lista de animales.animals.Add (animal);/ Notifica la lista de oyentes registrados. oyente.updateaniMaladded (animal));}}La mayor ventaja de la tecnología de la clase Mixin es encapsular el sujeto diseñado por el observador en una clase repetible, en lugar de repetir la lógica en cada clase de asignatura. Además, este método hace que la implementación de la clase del zoológico sea más simple, solo almacena información de animales sin considerar cómo almacenar y notificar a los oyentes.
Sin embargo, usar clases de mezcla no es solo una ventaja. Por ejemplo, ¿qué pasa si desea almacenar múltiples tipos de oyentes? Por ejemplo, también es necesario almacenar el tipo de oyente animalRemovedListener. La clase Mixin es una clase abstracta. No se pueden heredar múltiples clases abstractas al mismo tiempo en Java, y la clase Mixin no se puede implementar utilizando una interfaz. Esto se debe a que la interfaz no contiene estado, y el estado en el modo Observador debe usarse para guardar la lista de oyentes registrados.
Una solución es crear un zoolistener tipo oyente que se notifique cuando los animales aumenten y disminuyan. El código se ve así:
interfaz pública Zoolistener {public void onanimaladded (animal animal); public void onanimalremoved (animal animal);}De esta manera, puede usar esta interfaz para implementar el monitoreo de varios cambios en el estado del zoológico utilizando un tipo de oyente:
Clase pública ZoousingMixin extiende ObservablesUsubjectMixin <Soolistener> {Lista privada <animal> animales = new ArrayList <> (); public void Addanimal (animal animal) {// Agregue el animal a la lista de animales. escuchaLa combinación de múltiples tipos de oyentes en una interfaz del oyente resuelve el problema mencionado anteriormente, pero todavía hay deficiencias, que se discutirán en detalle en los siguientes capítulos.
Oyente y adaptador de métodos múltiples
En el método anterior, si la interfaz del oyente implementa demasiadas funciones, la interfaz será demasiado detallada. Por ejemplo, Swing Mouselistener contiene 5 funciones necesarias. Aunque solo puede usar una de ellas, debe agregar estas 5 funciones siempre que use el evento de clic del mouse. Es más probable que use cuerpos de funciones vacíos para implementar las funciones restantes, lo que sin duda traerá una confusión innecesaria al código.
Una solución es crear un adaptador (el concepto proviene del patrón de adaptador propuesto por GOF). El funcionamiento de la interfaz del oyente se implementa en forma de funciones abstractas para la herencia de la clase de oyente específica. De esta manera, la clase de oyente específica puede seleccionar las funciones que necesita y usar las operaciones predeterminadas para las funciones que no necesitan el adaptador. Por ejemplo, en la clase Zoolistener en el ejemplo anterior, cree ZooAdapter (las reglas de nomenclatura del adaptador son consistentes con el oyente, solo necesita cambiar el oyente en el nombre de clase a adaptador), el código es el siguiente:
Public Class ZooAdapter implementa Zoolistener {@OverridePublic Void onanimaladded (animal animal) {} @Overridepublic void onanimalremoved (animal animal) {}}A primera vista, esta clase de adaptador es insignificante, pero la conveniencia que trae no puede subestimarse. Por ejemplo, para las siguientes clases específicas, simplemente seleccione las funciones que les son útiles:
Clase pública NamePrinterzooAdapter extiende ZooAdapter {@OverridePublic Void onanimaladded (animal animal) {// imprima el nombre del animal que se agregó system.out.println ("animal agregado llamado" + animal.getName ());}}Hay dos alternativas que también pueden implementar las funciones de la clase de adaptador: una es usar la función predeterminada; El otro es fusionar la interfaz del oyente y la clase de adaptador en una clase específica. La función predeterminada es recientemente propuesta por Java 8, lo que permite a los desarrolladores proporcionar métodos de implementación predeterminados (defensa) en la interfaz.
Esta actualización de la Biblioteca Java es principalmente para facilitar a los desarrolladores a implementar extensiones del programa sin cambiar la versión anterior del código, por lo que este método debe usarse con precaución. Después de usarlo muchas veces, algunos desarrolladores sentirán que el código escrito de esta manera no es lo suficientemente profesional, y algunos desarrolladores piensan que esta es la característica de Java 8. No importa qué, necesitan comprender cuál es la intención original de esta tecnología y luego deciden si usarla en función de preguntas específicas. El código de interfaz Zoolistener implementado usando la función predeterminada es el siguiente:
interfaz pública Zoolistener {Public Public Void Onanimaladded (animal animal) {} public Public void onanimalremoved (animal animal) {}}Al usar las funciones predeterminadas, la implementación de las clases específicas de la interfaz no necesita implementar todas las funciones en la interfaz, sino implementar selectivamente las funciones requeridas. Aunque esta es una solución relativamente simple para el problema de expansión de la interfaz, los desarrolladores deben prestar más atención al usarlo.
La segunda solución es simplificar el modo Observador, omitir la interfaz del oyente y usar clases específicas para implementar las funciones del oyente. Por ejemplo, la interfaz Zoolistener se convierte en la siguiente:
clase pública Zoolistener {public void onanimaladded (animal animal) {} public void onanimalremoved (animal animal) {}}Esta solución simplifica la jerarquía del patrón del observador, pero no es aplicable a todos los casos, porque si la interfaz del oyente se fusiona en una clase específica, el oyente específico no puede implementar múltiples interfaces de escucha. Por ejemplo, si las interfaces AnimalAddedListener y AnimalRemovedListener están escritas en la misma clase de concreto, entonces un solo oyente específico no puede implementar ambas interfaces al mismo tiempo. Además, la intención de la interfaz del oyente es más obvia que la de la clase específica. Es obvio que el primero debe proporcionar interfaces para otras clases, pero la segunda no es tan obvia.
Sin la documentación adecuada, el desarrollador no sabrá que ya existe una clase que juega el papel de una interfaz e implementa todas sus funciones correspondientes. Además, el nombre de la clase no contiene adaptadores porque la clase no encaja en una determinada interfaz, por lo que el nombre de la clase no implica específicamente esta intención. Para resumir, un problema específico requiere elegir un método específico, y ningún método es omnipotente.
Antes de comenzar el próximo capítulo, es importante mencionar que los adaptadores son comunes en el modo de observación, especialmente en versiones anteriores del código Java. La API Swing se implementa en función de adaptadores, como usan muchas aplicaciones antiguas en el patrón de observador en Java 5 y Java 6. El oyente en el caso del zoológico puede no requerir un adaptador, pero debe comprender el propósito del adaptador y su aplicación, porque podemos usarlo en el código existente. El siguiente capítulo introducirá oyentes intensivos en el tiempo. Este tipo de oyente puede realizar operaciones que requieren mucho tiempo o hacer llamadas asincrónicas y no puede dar el valor de retorno de inmediato.
Oyente complejo y de bloqueo
Una suposición sobre el patrón del observador es que cuando se ejecuta una función, se llama a una serie de oyentes, pero se supone que este proceso es completamente transparente para la persona que llama. Por ejemplo, cuando el código del cliente agrega animal en el zoológico, no se sabe que se llamará a una serie de oyentes antes de que la devolución sea exitosa. Si la ejecución de un oyente lleva mucho tiempo (su tiempo se ve afectado por el número de oyentes, el tiempo de ejecución de cada oyente), el código del cliente será consciente de los efectos secundarios del tiempo de este simple aumento en las operaciones animales.
Este artículo no puede discutir este tema de manera integral. Las siguientes son las cosas a las que los desarrolladores deben prestar atención al llamar a los oyentes complejos:
El oyente comienza un nuevo hilo. Una vez que se inicia el nuevo hilo, mientras se ejecuta la lógica del oyente en el nuevo hilo, se devuelven los resultados de procesamiento de la función del oyente y se ejecutan otros oyentes.
El sujeto comienza un nuevo hilo. A diferencia de las iteraciones lineales tradicionales de las listas de oyentes registradas, la función de notificación de sujetos reinicia un nuevo hilo y luego itera sobre la lista de oyentes en el nuevo hilo. Esto permite que la función de notificación genere su valor de retorno mientras realiza operaciones de otros oyentes. Cabe señalar que se necesita un mecanismo de seguridad de subprocesos para garantizar que la lista de oyentes no experimente modificaciones concurrentes.
El oyente de cola llama y realiza funciones de escucha con un conjunto de hilos. Encapsular las operaciones del oyente en algunas funciones y colocarlas en lugar de una simple llamada iterativa a la lista de oyentes. Una vez que estos oyentes se almacenan en la cola, el hilo puede sacar un solo elemento de la cola y ejecutar su lógica de escucha. Esto es similar al problema del productor-consumidor. El proceso de notificación produce una cola de funciones ejecutables, que luego los subprocesos eliminan la cola a su vez y ejecutan estas funciones. La función necesita almacenar el tiempo que se creó en lugar del momento en que se ejecutó para que la función del oyente llame. Por ejemplo, una función creada cuando se llama al oyente, entonces la función debe almacenar el punto en el tiempo. Esta función es similar a las siguientes operaciones en Java:
clase pública AnimalAddedFunctor {Private Final AnimalAddedListener Listener; Private Final Animal Parameter; public animalAddedFunctor (animalAddedListener oyente, parámetro animal) {this.listener = oyente; this.parameter = parámetro;} public void ejecute () {// Ejecutar el oyente con el parámetro proporcionado durante el parámetro durante durante CreationThis.listener.updateaniMaladded (this.parameter);}}Las funciones se crean y guardan en una cola y pueden llamarse en cualquier momento, para que no haya necesidad de realizar sus operaciones correspondientes inmediatamente al atravesar la lista de la lista de la lista. Una vez que cada función que activa al oyente se empuja a la cola, el "hilo del consumidor" devolverá los derechos operativos al código del cliente. El "hilo de consumo" ejecutará estas funciones en algún momento más tarde, como si el oyente fuera activado por la función de notificación. Esta tecnología se denomina vinculación de parámetros en otros idiomas, que simplemente se ajusta al ejemplo anterior. La esencia de la tecnología es guardar los parámetros del oyente y luego llamar a la función Execute () directamente. Si el oyente recibe múltiples parámetros, el método de procesamiento es similar.
Cabe señalar que si desea guardar la orden de ejecución del oyente, debe introducir un mecanismo de clasificación integral. En el Esquema 1, el oyente activa nuevos hilos en orden normal, lo que garantiza que el oyente ejecute en el orden de registro. En el Esquema 2, las colas admiten la clasificación, y las funciones en ellas se ejecutarán en el orden en que ingresan a la cola. En pocas palabras, los desarrolladores deben prestar atención a la complejidad de la ejecución multiproceso de los oyentes y manejarlo cuidadosamente para garantizar que implementen las funciones requeridas.
Conclusión
Antes de que el modelo Observer se escribiera en el libro en 1994, ya era un modelo de diseño de software convencional, proporcionando muchas soluciones satisfactorias a los problemas que a menudo surgen en el diseño de software. Java siempre ha sido un líder en el uso de este patrón y encapsula este patrón en su biblioteca estándar, pero dado que Java se ha actualizado a la versión 8, es muy necesario volver a examinar el uso de patrones clásicos en ella. Con la aparición de expresiones lambda y otras estructuras nuevas, este patrón "antiguo" ha tomado una nueva vitalidad. Ya sea que se trate de manejar programas antiguos o utilizar este método de larga data para resolver nuevos problemas, especialmente para los desarrolladores de Java experimentados, el patrón Observer es la herramienta principal para los desarrolladores.
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 le presenta cómo usar Java8 para implementar el modo Observador (Parte 2), ¡espero que sea útil para todos!