[Patrones y paradigmas de diseño: conductuales] 57 | Modo observador (parte 2): ¿Cómo implementar un marco EventBus asíncrono y sin bloqueo?

En la última clase, aprendimos el principio, la implementación y los escenarios de aplicación del patrón Observer, centrándonos en varios métodos de implementación diferentes en diferentes escenarios de aplicación, que incluyen: método de bloqueo síncrono, no bloqueo asíncrono, en proceso y entre procesos. darse cuenta.

El bloqueo síncrono es el método de implementación más clásico, principalmente para el desacoplamiento de código; el no bloqueo asíncrono no solo puede lograr el desacoplamiento del código, sino también mejorar la eficiencia de la ejecución del código; el desacoplamiento en modo observador entre procesos es más completo, generalmente basado en la cola de mensajes que se usa para realizar la interacción entre lo observado y el observador entre diferentes procesos.

Hoy, nos enfocamos en el modo de observador asíncrono y sin bloqueo, y lo llevamos a implementar un marco general similar a Google Guava EventBus. Después de terminar esta lección, encontrará que no es difícil implementar un marco.

Sin más preámbulos, ¡comencemos oficialmente el estudio de hoy!

Implementación simple del patrón de observador asincrónico sin bloqueo

En la última lección, mencionamos que para el modo de observador asíncrono sin bloqueo, en realidad es muy fácil implementar una versión simple sin considerar la versatilidad y la reutilización.

Tenemos dos implementaciones. Uno de ellos es: crear un nuevo subproceso para ejecutar la lógica del código en cada función handleRegSuccess(); el otro es: usar el conjunto de subprocesos en la función register() de UserController para ejecutar la función handleRegSuccess() de cada observador. Los códigos específicos de las dos implementaciones son los siguientes:

// 第一种实现方式,其他类代码不变,就没有再重复罗列
public class RegPromotionObserver implements RegObserver {
  private PromotionService promotionService; // 依赖注入
  @Override
  public void handleRegSuccess(long userId) {
    Thread thread = new Thread(new Runnable() {
      @Override
      public void run() {
        promotionService.issueNewUserExperienceCash(userId);
      }
    });
    thread.start();
  }
}
// 第二种实现方式,其他类代码不变,就没有再重复罗列
public class UserController {
  private UserService userService; // 依赖注入
  private List<RegObserver> regObservers = new ArrayList<>();
  private Executor executor;
  public UserController(Executor executor) {
    this.executor = executor;
  }
  public void setRegObservers(List<RegObserver> observers) {
    regObservers.addAll(observers);
  }
  public Long register(String telephone, String password) {
    //省略输入参数的校验代码
    //省略userService.register()异常的try-catch代码
    long userId = userService.register(telephone, password);
    for (RegObserver observer : regObservers) {
      executor.execute(new Runnable() {
        @Override
        public void run() {
          observer.handleRegSuccess(userId);
        }
      });
    }
    return userId;
  }
}

Para el primer método de implementación, lleva mucho tiempo crear y destruir subprocesos con frecuencia, y la cantidad de subprocesos simultáneos no se puede controlar. La creación de demasiados subprocesos provocará un desbordamiento de la pila. Aunque el segundo método de implementación usa el grupo de subprocesos para resolver el problema del primer método de implementación, el grupo de subprocesos y la lógica de ejecución asincrónica se acoplan en la función register(), lo que aumenta el costo de mantenimiento de esta parte del código comercial.

Si nuestras necesidades son más extremas y necesitamos cambiar de manera flexible entre bloqueo síncrono y no bloqueo asíncrono, debemos modificar constantemente el código de UserController. Además, si más de un módulo comercial necesita usar el modo de observador sin bloqueo asíncrono en el proyecto, dicha implementación de código no se puede reutilizar.

Sabemos que la función del marco es ocultar los detalles de implementación, reducir la dificultad de desarrollo, lograr la reutilización del código, desvincular el código comercial y no comercial, y permitir que los programadores se concentren en el desarrollo comercial. Para el modo de observador asíncrono sin bloqueo, también podemos abstraerlo en un marco para lograr este efecto, y este marco es el EventBus del que hablaremos en esta lección.

Introducción a los requisitos funcionales de EventBus Framework

EventBus se traduce como "bus de eventos", que proporciona el código básico para implementar el patrón del observador. Con base en este marco, podemos implementar fácilmente el patrón Observer en nuestros propios escenarios comerciales sin desarrollar desde cero. Entre ellos, Google Guava EventBus es un marco EventBus muy conocido, que no solo admite el modo sin bloqueo asíncrono, sino que también admite el modo de bloqueo síncrono.

Ahora, echemos un vistazo a las funciones que tiene Guava EventBus. Todavía el ejemplo de registro de usuario en la última lección, vamos a volver a implementarlo con Guava EventBus, el código es el siguiente:

public class UserController {
  private UserService userService; // 依赖注入
  private EventBus eventBus;
  private static final int DEFAULT_EVENTBUS_THREAD_POOL_SIZE = 20;
  public UserController() {
    //eventBus = new EventBus(); // 同步阻塞模式
    eventBus = new AsyncEventBus(Executors.newFixedThreadPool(DEFAULT_EVENTBUS_THREAD_POOL_SIZE)); // 异步非阻塞模式
  }
  public void setRegObservers(List<Object> observers) {
    for (Object observer : observers) {
      eventBus.register(observer);
    }
  }
  public Long register(String telephone, String password) {
    //省略输入参数的校验代码
    //省略userService.register()异常的try-catch代码
    long userId = userService.register(telephone, password);
    eventBus.post(userId);
    return userId;
  }
}
public class RegPromotionObserver {
  private PromotionService promotionService; // 依赖注入
  @Subscribe
  public void handleRegSuccess(long userId) {
    promotionService.issueNewUserExperienceCash(userId);
  }
}
public class RegNotificationObserver {
  private NotificationService notificationService;
  @Subscribe
  public void handleRegSuccess(long userId) {
    notificationService.sendInboxMessage(userId, "...");
  }
}

En comparación con el modo de observador escrito desde cero, el modo de observador implementado mediante el marco EventBus es aproximadamente el mismo en términos de proceso a gran escala. Ambos deben definir el Observador y registrar el Observador a través de la función register(), y también deben pass Llame a una función (por ejemplo, la función post() en EventBus) para enviar un mensaje al observador (un mensaje se denomina evento en EventBus).

Sin embargo, en términos de detalles de implementación, son algo diferentes. Basado en EventBus, no necesitamos definir la interfaz de Observer, y cualquier tipo de objeto se puede registrar en EventBus, y la anotación @Subscribe se usa para indicar qué función en la clase puede recibir el mensaje enviado por el observador.

A continuación, hablemos en detalle de varias clases y funciones principales de Guava EventBus.

  • EventBus、AsyncEventBus

Todas las interfaces llamables expuestas por Guava EventBus están encapsuladas en la clase EventBus. Entre ellos, EventBus implementa el modo de observador de bloqueo síncrono y AsyncEventBus hereda de EventBus, proporcionando un modo de observador asíncrono sin bloqueo. El uso específico es el siguiente:

EventBus eventBus = new EventBus(); // 同步阻塞模式
EventBus eventBus = new AsyncEventBus(Executors.newFixedThreadPool(8));// 异步阻塞模式
  • función de registro ()

La clase EventBus proporciona la función register() para registrar observadores. La definición de función específica es la siguiente. Puede aceptar observadores de cualquier tipo (Objeto). En la implementación del patrón clásico de Observer, la función register() debe aceptar un objeto de clase que implemente la misma interfaz de Observer.

public void register(Object object);
  • función anular registro()

En relación con la función register(), la función unregister() se utiliza para eliminar un observador del EventBus. No explicaré mucho, la definición específica de la función es la siguiente:

public void unregister(Object object);
  • función de publicación ()

La clase EventBus proporciona la función post() para enviar mensajes a los observadores. La definición específica de la función es la siguiente:

public void post(Object event);

La diferencia con el modo observador clásico es que cuando llamamos a la función post() para enviar un mensaje, el mensaje no se envía a todos los observadores, sino a los observadores coincidentes. El llamado emparejamiento significa que el tipo de mensaje que se puede recibir es la clase principal del tipo de mensaje enviado (evento en la definición de la función de publicación). Dejame explicarte con un ejemplo.

Por ejemplo, el tipo de mensaje que puede recibir AObserver es XMsg, el tipo de mensaje que puede recibir BObserver es YMsg y el tipo de mensaje que puede recibir CObserver es ZMsg. Entre ellos, XMsg es la clase principal de YMsg. Cuando enviamos un mensaje de la siguiente manera, los correspondientes observadores coincidentes que pueden recibir el mensaje son los siguientes:

XMsg xMsg = new XMsg();
YMsg yMsg = new YMsg();
ZMsg zMsg = new ZMsg();
post(xMsg); => AObserver接收到消息
post(yMsg); => AObserver、BObserver接收到消息
post(zMsg); => CObserver接收到消息

Usted puede preguntar, ¿dónde está definido el tipo de mensaje que puede recibir cada observador? Echemos un vistazo a una de las características más especiales de Guava EventBus, que es la anotación @Subscribe.

  • @Suscribir anotación

EventBus usa la anotación @Subscribe para indicar qué tipo de mensaje puede recibir una función. El código de uso específico es el siguiente. En la clase DObserver, hemos anotado dos funciones f1() y f2() a través de @Subscribe.

public DObserver {
  //...省略其他属性和方法...
  
  @Subscribe
  public void f1(PMsg event) { //... }
  
  @Subscribe
  public void f2(QMsg event) { //... }
}

Cuando el objeto de la clase DObserver se registra en EventBus a través de la función register(), EventBus encontrará f1() y f2() según la anotación @Subscribe y registrará los tipos de mensajes que las dos funciones pueden recibir (PMsg->f1, QMsj -> f2). Cuando enviamos un mensaje (como un mensaje QMsg) a través de la función post(), EventBus llamará a la función correspondiente (f2) a través del registro anterior (QMsg->f2).

Implementar un marco EventBus a mano

Hemos explicado claramente la función de Guava EventBus y, en términos generales, es relativamente simple. A continuación, repetiremos la creación de la rueda y "casa de campo" un EventBus.

Centrémonos en los principios de implementación de las dos funciones principales register() y post() en EventBus. Después de comprenderlos, básicamente comprende todo el marco de EventBus. Las siguientes dos figuras son los diagramas esquemáticos de implementación de estas dos funciones.

inserte la descripción de la imagen aquí

inserte la descripción de la imagen aquí
Podemos ver en la figura que la estructura de datos más crítica es el registro Observer, que registra la correspondencia entre los tipos de mensajes y las funciones que pueden recibir mensajes. Al llamar a la función register() para registrar observadores, EventBus genera el registro de observadores analizando la anotación @Subscribe. Cuando se llama a la función post() para enviar un mensaje, EventBus encuentra la función correspondiente que puede recibir el mensaje a través del registro y luego crea dinámicamente el objeto y ejecuta la función a través de la sintaxis de reflexión de Java. Para el modo de bloqueo síncrono, EventBus ejecuta las funciones correspondientes secuencialmente en un hilo. Para el modo sin bloqueo asíncrono, EventBus ejecuta las funciones correspondientes a través de un grupo de subprocesos.

Una vez que comprende el principio, es mucho más fácil de implementar. La implementación del código de todo el marco pequeño incluye 5 clases: EventBus, AsyncEventBus, Subscribe, ObserverAction, ObserverRegistry. A continuación, veamos estas cinco clases por separado.

1.Suscríbete

Subscribe es una anotación utilizada para indicar qué función en el observador puede recibir mensajes.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Beta
public @interface Subscribe {}

2.ObservadorAcción

La clase ObserverAction se usa para representar el método de anotación @Subscribe, donde el objetivo representa la clase del observador y el método representa el método. Se utiliza principalmente en el registro de observadores ObserverRegistry.

public class ObserverAction {
  private Object target;
  private Method method;
  public ObserverAction(Object target, Method method) {
    this.target = Preconditions.checkNotNull(target);
    this.method = method;
    this.method.setAccessible(true);
  }
  public void execute(Object event) { // event是method方法的参数
    try {
      method.invoke(target, event);
    } catch (InvocationTargetException | IllegalAccessException e) {
      e.printStackTrace();
    }
  }
}

3. Registro de observadores

La clase ObserverRegistry es el Registro de Observadores mencionado anteriormente, y es la clase más compleja Casi toda la lógica central en el marco está en esta clase. Esta clase usa ampliamente la sintaxis de reflexión de Java, pero el código como un todo no es difícil de entender.Entre ellos, uno de los lugares más hábiles es el uso de CopyOnWriteArraySet.

CopyOnWriteArraySet, como su nombre lo indica, crea un nuevo conjunto al escribir datos y clona los datos originales en el nuevo conjunto.Después de escribir datos en el nuevo conjunto, reemplaza el antiguo conjunto con el nuevo conjunto. De esta forma, se puede asegurar que cuando se escriben datos, la operación de lectura de datos no se verá afectada, para resolver el problema de la lectura y escritura simultáneas. Además, CopyOnWriteSet también evita conflictos de escritura simultáneos mediante el bloqueo. Para la función específica, puede verificar el código fuente de la clase CopyOnWriteSet, que es claro de un vistazo.

public class ObserverRegistry {
  private ConcurrentMap<Class<?>, CopyOnWriteArraySet<ObserverAction>> registry = new ConcurrentHashMap<>();
  public void register(Object observer) {
    Map<Class<?>, Collection<ObserverAction>> observerActions = findAllObserverActions(observer);
    for (Map.Entry<Class<?>, Collection<ObserverAction>> entry : observerActions.entrySet()) {
      Class<?> eventType = entry.getKey();
      Collection<ObserverAction> eventActions = entry.getValue();
      CopyOnWriteArraySet<ObserverAction> registeredEventActions = registry.get(eventType);
      if (registeredEventActions == null) {
        registry.putIfAbsent(eventType, new CopyOnWriteArraySet<>());
        registeredEventActions = registry.get(eventType);
      }
      registeredEventActions.addAll(eventActions);
    }
  }
  public List<ObserverAction> getMatchedObserverActions(Object event) {
    List<ObserverAction> matchedObservers = new ArrayList<>();
    Class<?> postedEventType = event.getClass();
    for (Map.Entry<Class<?>, CopyOnWriteArraySet<ObserverAction>> entry : registry.entrySet()) {
      Class<?> eventType = entry.getKey();
      Collection<ObserverAction> eventActions = entry.getValue();
      if (postedEventType.isAssignableFrom(eventType)) {
        matchedObservers.addAll(eventActions);
      }
    }
    return matchedObservers;
  }
  private Map<Class<?>, Collection<ObserverAction>> findAllObserverActions(Object observer) {
    Map<Class<?>, Collection<ObserverAction>> observerActions = new HashMap<>();
    Class<?> clazz = observer.getClass();
    for (Method method : getAnnotatedMethods(clazz)) {
      Class<?>[] parameterTypes = method.getParameterTypes();
      Class<?> eventType = parameterTypes[0];
      if (!observerActions.containsKey(eventType)) {
        observerActions.put(eventType, new ArrayList<>());
      }
      observerActions.get(eventType).add(new ObserverAction(observer, method));
    }
    return observerActions;
  }
  private List<Method> getAnnotatedMethods(Class<?> clazz) {
    List<Method> annotatedMethods = new ArrayList<>();
    for (Method method : clazz.getDeclaredMethods()) {
      if (method.isAnnotationPresent(Subscribe.class)) {
        Class<?>[] parameterTypes = method.getParameterTypes();
        Preconditions.checkArgument(parameterTypes.length == 1,
                "Method %s has @Subscribe annotation but has %s parameters."
                        + "Subscriber methods must have exactly 1 parameter.",
                method, parameterTypes.length);
        annotatedMethods.add(method);
      }
    }
    return annotatedMethods;
  }
}

4. Autobús de eventos

EventBus implementa un patrón de observador síncrono de bloqueo. Mirando el código, es posible que tenga algunas dudas.Esto obviamente usa el Ejecutor del grupo de subprocesos. De hecho, MoreExecutors.directExecutor() es una clase de herramienta proporcionada por Google Guava. Parece ser de varios subprocesos, pero en realidad es de un solo subproceso. El motivo de esta implementación es principalmente unificar la lógica del código con AsyncEventBus y lograr la reutilización del código.

public class EventBus {
  private Executor executor;
  private ObserverRegistry registry = new ObserverRegistry();
  public EventBus() {
    this(MoreExecutors.directExecutor());
  }
  protected EventBus(Executor executor) {
    this.executor = executor;
  }
  public void register(Object object) {
    registry.register(object);
  }
  public void post(Object event) {
    List<ObserverAction> observerActions = registry.getMatchedObserverActions(event);
    for (ObserverAction observerAction : observerActions) {
      executor.execute(new Runnable() {
        @Override
        public void run() {
          observerAction.execute(event);
        }
      });
    }
  }
}

5.AsyncEventBus

Con EventBus, la implementación de AsyncEventBus es muy sencilla. Para implementar el modo de observador asíncrono sin bloqueo, ya no puede seguir usando MoreExecutors.directExecutor(), pero la persona que llama en el constructor debe inyectarlo en el grupo de subprocesos.

public class AsyncEventBus extends EventBus {
  public AsyncEventBus(Executor executor) {
    super(executor);
  }
}

Hasta ahora, hemos usado menos de 200 líneas de código para implementar un EventBus bastante utilizable, funcionalmente hablando, es casi lo mismo que Google Guava EventBus. Sin embargo, si observa el código fuente de Google Guava EventBus, encontrará que, en términos de detalles de implementación, en comparación con nuestra implementación actual, en realidad ha realizado muchas optimizaciones, como optimizar el algoritmo para encontrar funciones de coincidencia de mensajes. en el registro Si tiene tiempo, le sugiero que lea su código fuente.

revisión clave

Bueno, eso es todo por el contenido de hoy. Resumamos y revisemos el contenido que necesita dominar.

Las funciones del marco incluyen: ocultar los detalles de implementación, reducir la dificultad de desarrollo, lograr la reutilización del código, desacoplar los códigos comerciales y no comerciales y permitir que los programadores se centren en el desarrollo comercial. Para el modo de observador asíncrono sin bloqueo, también podemos abstraerlo en un marco para lograr este efecto, y este marco es el EventBus del que hablamos en esta lección. EventBus se traduce como "bus de eventos", que proporciona el código básico para implementar el patrón del observador. Con base en este marco, podemos implementar fácilmente el patrón Observer en nuestros propios escenarios comerciales sin desarrollar desde cero.

Mucha gente piensa que no hay ningún desafío técnico en el desarrollo de negocios. De hecho, el desarrollo de negocios también implica el desarrollo de muchas funciones no comerciales, como el EventBus mencionado hoy. En el desarrollo comercial normal, debemos ser buenos para abstraer estas funciones no comerciales y reutilizables, e implementarlas activamente en un marco común.

discusión en clase

En el segundo módulo del contenido de hoy "Introducción a los requisitos funcionales de EventBus Framework", reimplementamos UserController con Guava EventBus. De hecho, el código aún no está lo suficientemente desacoplado. UserController aún combina una gran cantidad de código no comercial relacionado con el patrón Observer, como la creación de grupos de subprocesos y el registro de Observers. ¿Tiene alguna sugerencia de refactorización para hacer que UserController esté más centrado en el negocio?

Supongo que te gusta

Origin blog.csdn.net/qq_32907491/article/details/131179621
Recomendado
Clasificación