[Design Patterns and Paradigms: Behavioral] 57 | Modo Observer (Parte 2): Como implementar um framework EventBus assíncrono e sem bloqueio?

Na última aula, aprendemos o princípio, a implementação e os cenários de aplicação do padrão Observer, focando em vários métodos de implementação diferentes em diferentes cenários de aplicação, incluindo: método de bloqueio síncrono, não bloqueio assíncrono, método em processo e método entre processos constatar.

O bloqueio síncrono é o método de implementação mais clássico, principalmente para desacoplamento de código; o não-bloqueio assíncrono pode não apenas alcançar o desacoplamento de código, mas também melhorar a eficiência da execução do código; o desacoplamento do modo observador entre os processos é mais completo, geralmente baseado na fila de mensagens é usado para realizar a interação entre o observado e o observador entre diferentes processos.

Hoje, nos concentramos no modo observador assíncrono e sem bloqueio e levamos você a implementar uma estrutura geral semelhante ao Google Guava EventBus. Depois de terminar esta lição, você descobrirá que não é difícil implementar um framework.

Sem mais delongas, vamos começar oficialmente o estudo de hoje!

Implementação simples do padrão de observador assíncrono sem bloqueio

Na última lição, mencionamos que, para o modo de observador assíncrono e sem bloqueio, é realmente muito fácil implementar uma versão simples sem considerar qualquer versatilidade e reutilização.

Temos duas implementações. Uma delas é: criar uma nova thread para executar a lógica do código em cada função handleRegSuccess(); a outra é: usar o pool de threads na função register() do UserController para executar a função handleRegSuccess() de cada observador. Os códigos específicos das duas implementações são os seguintes:

// 第一种实现方式,其他类代码不变,就没有再重复罗列
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 o primeiro método de implementação, é demorado criar e destruir encadeamentos com frequência, e o número de encadeamentos simultâneos não pode ser controlado. Criar muitos encadeamentos causará estouro de pilha. Embora o segundo método de implementação use o pool de threads para resolver o problema do primeiro método de implementação, o pool de threads e a lógica de execução assíncrona são acoplados na função register(), o que aumenta o custo de manutenção dessa parte do código de negócios.

Se nossas necessidades forem mais extremas e precisarmos alternar com flexibilidade entre bloqueio síncrono e não bloqueio assíncrono, devemos modificar constantemente o código de UserController. Além disso, se mais de um módulo de negócios precisar usar o modo de observador assíncrono sem bloqueio no projeto, essa implementação de código não poderá ser reutilizada.

Sabemos que o papel da estrutura é ocultar os detalhes da implementação, reduzir a dificuldade de desenvolvimento, obter a reutilização de código, separar códigos comerciais e não comerciais e permitir que os programadores se concentrem no desenvolvimento de negócios. Para o modo de observador assíncrono sem bloqueio, também podemos abstraí-lo em uma estrutura para obter esse efeito, e essa estrutura é o EventBus sobre o qual falaremos nesta lição.

Introdução aos Requisitos Funcionais do EventBus Framework

EventBus é traduzido como "ônibus de evento", que fornece o código esqueleto para implementar o padrão do observador. Com base nessa estrutura, podemos implementar facilmente o padrão Observer em nossos próprios cenários de negócios sem desenvolver do zero. Entre eles, o Google Guava EventBus é uma estrutura EventBus bem conhecida, que não apenas suporta o modo assíncrono sem bloqueio, mas também suporta o modo de bloqueio síncrono

Agora, vamos dar uma olhada em quais funções o Guava EventBus tem. Ainda o exemplo de cadastro de usuário da última aula, vamos reimplementar com Guava EventBus, o código é o seguinte:

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, "...");
  }
}

Comparado com o modo observer escrito do zero, o modo observer implementado usando a estrutura EventBus é aproximadamente o mesmo em termos de processo em larga escala. Ambos precisam definir o Observer e registrar o Observer por meio da função register() e também precisam pass Chame uma função (por exemplo, a função post() em EventBus) para enviar uma mensagem ao Observer (uma mensagem é chamada de evento em EventBus).

No entanto, em termos de detalhes de implementação, eles são um pouco diferentes. Com base no EventBus, não precisamos definir a interface do Observer, podendo ser cadastrado qualquer tipo de objeto no EventBus, sendo utilizada a anotação @Subscribe para indicar qual função da classe pode receber a mensagem enviada pelo observador.

A seguir, vamos falar sobre várias classes principais e funções do Guava EventBus em detalhes.

  • EventBus, AsyncEventBus

Todas as interfaces chamáveis ​​expostas pelo Guava EventBus são encapsuladas na classe EventBus. Entre eles, EventBus implementa o modo de observador de bloqueio síncrono e AsyncEventBus herda de EventBus, fornecendo um modo de observador assíncrono sem bloqueio. O uso específico é o seguinte:

EventBus eventBus = new EventBus(); // 同步阻塞模式
EventBus eventBus = new AsyncEventBus(Executors.newFixedThreadPool(8));// 异步阻塞模式
  • função registrar()

A classe EventBus fornece a função register() para registrar observadores. A definição da função específica é a seguinte. Pode aceitar observadores de qualquer tipo (objeto). Na implementação do padrão Observer clássico, a função register() deve aceitar um objeto de classe que implemente a mesma interface Observer.

public void register(Object object);
  • função cancelar registro ()

Em relação à função register(), a função unregister() é usada para remover um observador do EventBus. Não vou explicar muito, a definição específica da função é a seguinte:

public void unregister(Object object);
  • função post()

A classe EventBus fornece a função post() para enviar mensagens aos observadores. A definição da função específica é a seguinte:

public void post(Object event);

A diferença com o modo observador clássico é que quando chamamos a função post() para enviar uma mensagem, a mensagem não é enviada para todos os observadores, mas para os observadores correspondentes. O chamado matchable significa que o tipo de mensagem que pode ser recebido é a classe pai do tipo da mensagem enviada (evento na definição da função post). Deixe-me explicar com um exemplo.

Por exemplo, o tipo de mensagem que o AObserver pode receber é XMsg, o tipo de mensagem que o BObserver pode receber é YMsg e o tipo de mensagem que o CObserver pode receber é ZMsg. Entre eles, XMsg é a classe pai de YMsg. Quando enviamos uma mensagem da seguinte forma, os observadores compatíveis correspondentes que podem receber a mensagem são os seguintes:

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

Você pode perguntar, onde está definido o tipo de mensagem que cada Observer pode receber? Vamos dar uma olhada em um dos recursos mais especiais do Guava EventBus, que é a anotação @Subscribe.

  • Anotação @Subscribe

EventBus usa a anotação @Subscribe para indicar qual tipo de mensagem uma função pode receber. O código de uso específico é o seguinte. Na classe DObserver, anotamos duas funções f1() e f2() via @Subscribe.

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

Quando o objeto da classe DObserver é registrado no EventBus através da função register(), o EventBus encontrará f1() e f2() de acordo com a anotação @Subscribe e registrará os tipos de mensagem que as duas funções podem receber (PMsg->f1, QMsg -> f2). Quando enviamos uma mensagem (como uma mensagem QMsg) através da função post(), o EventBus irá chamar a função correspondente (f2) através do registro anterior (QMsg->f2).

Implemente uma estrutura EventBus manualmente

Explicamos claramente a função do Guava EventBus e, de modo geral, é relativamente simples. A seguir, repetiremos a criação da roda e "chalé" um EventBus.

Vamos nos concentrar nos princípios de implementação das duas funções principais register() e post() no EventBus. Depois de entendê-los, você basicamente entende toda a estrutura do EventBus. As duas figuras a seguir são os diagramas esquemáticos de implementação dessas duas funções.

insira a descrição da imagem aqui

insira a descrição da imagem aqui
Podemos ver na figura que a estrutura de dados mais crítica é o registro do Observer, que registra a correspondência entre os tipos de mensagem e as funções que podem receber mensagens. Ao chamar a função register() para registrar observadores, o EventBus gera o registro do Observador analisando a anotação @Subscribe. Quando a função post() é chamada para enviar uma mensagem, o EventBus encontra a função correspondente que pode receber a mensagem por meio do registro e, em seguida, cria dinamicamente o objeto e executa a função por meio da sintaxe de reflexão Java. Para o modo de bloqueio síncrono, o EventBus executa as funções correspondentes sequencialmente em um thread. Para o modo sem bloqueio assíncrono, o EventBus executa as funções correspondentes por meio de um pool de threads.

Depois de entender o princípio, é muito mais fácil implementá-lo. A implementação de código de toda a pequena estrutura inclui 5 classes: EventBus, AsyncEventBus, Subscribe, ObserverAction, ObserverRegistry. Em seguida, vamos olhar para essas cinco classes por sua vez.

1.Inscrever-se

Subscribe é uma anotação usada para indicar qual função no observador pode receber mensagens.

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

2.Ação do Observador

A classe ObserverAction é usada para representar o método de anotação @Subscribe, onde target representa a classe do observador e method representa o método. É usado principalmente no registro do observador 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 do Observador

A classe ObserverRegistry é o Observer Registry mencionado acima e é a classe mais complexa. Quase toda a lógica central da estrutura está nesta classe. Essa classe usa extensivamente a sintaxe de reflexão do Java, mas o código como um todo não é difícil de entender.Entre eles, um dos lugares mais habilidosos é o uso de CopyOnWriteArraySet.

CopyOnWriteArraySet, como o nome indica, cria um novo conjunto ao gravar dados e clona os dados originais no novo conjunto. Depois de gravar os dados no novo conjunto, substitui o antigo conjunto pelo novo conjunto. Dessa forma, pode-se garantir que, quando os dados forem gravados, a operação de leitura dos dados não seja afetada, de modo a resolver o problema de leitura e gravação simultâneas. Além disso, CopyOnWriteSet também evita conflitos de gravação simultâneos por bloqueio. Para a função específica, você pode verificar o código-fonte da classe CopyOnWriteSet, que fica claro à primeira vista.

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.EventBus

EventBus implementa um padrão de observador síncrono de bloqueio. Olhando o código, você pode ter algumas dúvidas, pois obviamente utiliza o thread pool Executor. Na verdade, MoreExecutors.directExecutor() é uma classe de ferramenta fornecida pelo Google Guava. Parece ser multi-threaded, mas na verdade é single-threaded. O motivo dessa implementação é principalmente unificar a lógica do código com AsyncEventBus e obter a reutilização do 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

Com EventBus, a implementação de AsyncEventBus é muito simples. Para implementar o modo observador assíncrono sem bloqueio, ele não pode mais continuar a usar MoreExecutors.directExecutor(), mas precisa ser injetado no pool de encadeamentos pelo chamador no construtor.

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

Até agora, usamos menos de 200 linhas de código para implementar um EventBus razoavelmente utilizável, funcionalmente falando, é quase o mesmo que o Google Guava EventBus. No entanto, se você observar o código-fonte do Google Guava EventBus, descobrirá que, em termos de detalhes de implementação, em comparação com nossa implementação atual, ele realmente fez muitas otimizações, como otimizar o algoritmo para encontrar funções de correspondência de mensagens no registro. Se você tiver tempo, sugiro que leia o código-fonte.

revisão chave

Bem, isso é tudo para o conteúdo de hoje. Vamos resumir e revisar o conteúdo que você precisa dominar.

As funções da estrutura incluem: ocultar detalhes de implementação, reduzir a dificuldade de desenvolvimento, alcançar a reutilização de código, desacoplar códigos comerciais e não comerciais e permitir que os programadores se concentrem no desenvolvimento de negócios. Para o modo observador sem bloqueio assíncrono, também podemos abstraí-lo em uma estrutura para obter esse efeito, e essa estrutura é o EventBus sobre o qual falamos nesta lição. EventBus é traduzido como "ônibus de evento", que fornece o código esqueleto para implementar o padrão do observador. Com base nessa estrutura, podemos implementar facilmente o padrão Observer em nossos próprios cenários de negócios sem desenvolver do zero.

Muitas pessoas pensam que não há desafio técnico no desenvolvimento de negócios.Na verdade, o desenvolvimento de negócios também envolve o desenvolvimento de muitas funções não comerciais, como o EventBus mencionado hoje. No desenvolvimento de negócios normal, devemos ser bons em abstrair essas funções não comerciais e reutilizáveis ​​e implementá-las ativamente em uma estrutura comum.

discussão em classe

No segundo módulo do conteúdo de hoje "Introdução aos Requisitos Funcionais do Framework EventBus", reimplementamos UserController com Guava EventBus. Na verdade, o código ainda não está desacoplado o suficiente. O UserController ainda acopla muitos códigos não comerciais relacionados ao padrão Observer, como a criação de pools de threads e o registro de Observers. Você tem alguma sugestão de refatoração para tornar o UserController mais focado nos negócios?

Acho que você gosta

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