Orientação ao Objeto Reativa

Um estudo de implementação de sistemas reativos

Topericídio Gaiden

A essa altura do campeonato não é mais novidade para ninguém que sou um grande fã de sistemas reativos. Ou pelo menos não deveria ser.

Se isso ainda não está explícito, permita-me me apresentar: Oi, sou o Virgs e eu amo sistemas reativos e odeio sistemas proativos. Ah, além de nutrir esse ódio voraz, escrevi os seguintes textos a respeito:

Como podem perceber, destilar meu ódio sobre o assunto é meio que um hobby pra mim e serve para aliviar minhas tensões.

Esse texto é mais um desses com propósito terapêutico e começa com uma simples pergunta: como seria desenvolver um projeto usando práticas dos sistemas reativos?

Dado que há uma relação inegável entre arquitetura de microsserviços e de software. Onde ambas exigem alta coesão e baixo acoplamento e outros princípios como SOLID e Don't Repeat Yourself, também são perfeitamente aplicáveis. O que eu aprenderia ao simular um ecossistema reativo no nível de abstração de orientação ao objeto? Quais seriam os desafios e aprendizados encontrados?

Seria um projeto mais didático do que difícil, uma vez que arquitetura de classes está mais próxima do nosso cotidiano, portanto, mais fácil de assimilar. O importante seria o código, logo, sem infraestrutura, sem pipeline ou cloud providers.

Nesse nível de abstração, as classes funcionariam como serviços. Os objetos seriam as instâncias de cada serviço e o gerenciador de eventos (mediator, broker, delegate…) simularia um protocolo de comunicação, seja ele qual for.

Desse modo, eu poderia compartilhar os próprios códigos, ao invés de diagramas representando os serviços.

Orientação ao Objeto Reativa

Para implementar essa abstração, que eu, com o poder investido em mim, por mim mesmo, oficialmente, batizei de orientação ao objeto reativa, elaborei três regras:

  1. Abolir todos os getters: o método get simboliza a própria reencarnação da proatividade em forma de orientação ao objeto. Nessa abordagem, é o demônio em forma de código. O método, literalmente, funciona como um meio para solicitar, proativamente, um dado que pertence a um outro objeto. Contrariando o princípio Tell, Don't Ask (livremente traduzido como Diga, Não Solicite) e indo frontalmente contra o conceito de reatividade.
  2. A comunicação entre objetos de negócio deve ser feita exclusivamente por eventos: sem métodos públicos, com exceção dos construtores. Classes de negócio não sabem da existência uma da outra. Essa regra permite que o sistema rode de maneira assíncrona e, principalmente, independente. (Analisando enquanto escrevo, percebo que essa regra elimina a necessidade da primeira: NO-GETTER-RULE. Mas como tenho raiva dos getter também, resolvi deixar a regra lá. Faz parte da terapia).
  3. Mensagens devem ser stateless: se você implementa sistemas RESTful, essa vai ser teta. Na prática, isso significa que as mensagens devem incluir toda informação suficiente e necessária para que os receptores as processem de maneira adequada.

Definidas as regras que eu deveria seguir para a implementação do projeto, faltava definir do que esse projeto se trataria.

Uma antiga paixão falou mais alto e eu decidi fazer um jogo. Um não, minha incapacidade de decisão me obrigou a fazer três. A medida que os projetos foram nascendo, mais fácil era satisfazer essa três regras. De modo que o último projeto, tetris, não viola nenhuma.

Como plataforma alvo, escolhi os browsers assim o projeto seria multiplataforma de brinde. Usaria a linguagem de programação compatível e que implementa as especificações da orientação ao objeto typescript e uma biblioteca de jogos compatível phaser.

Eis os repositórios do github e os respectivos links jogáveis, para caso queiram se divertir com os códigos ou analisar os jogos:

  1. Topericídio Gaiden (repositório)

2. Buraco de Cobra (repositório)

3. Tetris (repositório)

Apesar de tentador, não permitam que meu inegável talento artístico impressione. O texto é sobre desenvolvimento de software.

Plataforma, linguagem, biblioteca, regras, projetos… Tudo definido, hora do vamo ver.

Hora do vamo ver

Por motivos de texto-muito-longo e ser um bom estudo de caso, me atenho apenas aos destaques do projeto de tetris.

Serviços/Objetos

De cara (nem tão de cara assim), identifiquei três classes (que atuariam como microsserviços em um outro nível de abstração):

  • InputManager: identifica os comandos do usuário (esquerda, direita, rotação…).
  • AliveBlockHandler: manuseio e verificação de colisão do bloco ativo, aquele que está caindo. Ao receber um comando do usuário, deve aplicá-lo ao bloco atual.
  • BoardHandler: administra os blocos inativos e cria novos blocos ativos.

A classe principal da biblioteca, a que contém o GameLoop ficou parecendo com isso:

Principais pontos de main-scene.ts

O método create instancia os objetos. Eles são "perdidos", ou seja, não são atribuídos a nenhum membro e nenhuma referência a eles existe.

No método update, destaco os eventos GO_DOWN_ONE_LEVEL e UPDATE que controlam a cadência do jogo.

A classe responsável por gerenciar o tabuleiro, ficou parecida com isso:

Principais pontos de board-handler.ts

A classe escuta dois eventos: BLOCK_DIED e BOARD_CREATE_NEW_BLOCK. No primeiro evento, verifica se alguma linha está completa e, portanto, deve ser eliminada. Após isso, redesenha a tela.

Já no segundo evento, quando recebe a mensagem solicitando a criação de um novo bloco cadente, aleatoriamente define o novo bloco a ser criado e informa quais células estão mortas no tabuleiro. Ponto importante já que o bloco cadente deve saber como detectar colisão com os blocos remanescentes no fundo.

Ilustração do fluxo de criação de blocos

Um último código que apresento, é o da classe que manuseia o bloco cadente:

alive-block-handler.ts

Saliento dois pontos:

  1. Linha 8: o gerenciador do bloco percebe que um novo bloco foi criado. Identifica a forma desse bloco, reposiciona e atualiza sua lista de blocos inativos para poder detectar a colisão futuramente.
  2. Método goDownOneLevel. Simula a próxima posição do bloco. Caso uma colisão seja detectada, informa que o bloco não é mais ativo a posição final de cada célula. Dessa forma, a classe gerenciadora do tabuleiro conseguirá detectar a eliminação de linhas.
Ciclo de vida do bloco cadente.

Na abordagem OO tradicional e sem as três premissas que elaborei, seria mais intuitivo fazer com que as classes BoardHandler e AliveBlockHandler conhecessem uma a outra. A AliveBlockHandler poderia, por exemplo, requisitar (get) as posições de todos os blocos inativos de BoardHandler para verificar a colisão. Assim como a classe que gerencia o input do usuário poderia conhecer a instância da classe AliveBlockHandler para executar uma ação diretamente (invocar algum método público).

Não é o que acontece na abordagem reativa. Sem o conhecimento uma das outras, os componentes podem ser verdadeiramente autônomos e independentes, conceitos fundamentais na construção de uma arquitetura de microsserviços. Assim, se um serviço estiver fora do ar, os outros continuam funcionando perfeitamente, evitando catastróficas reações em cadeia.

Reforço que o repositório consta no meu github. Recomendo fortemente que verifiquem se quiserem entender mais profundamente. Certamente outros pontos também chamarão atenção.

Após o desenvolvimento dos três projetos, acho que tenho insumo suficiente para ter uma opinião minimamente embasada.

Conclusão

Mais difícil do que parece

Não sei ao certo se a dificuldade é apenas a falta de prática ou se é uma dificuldade intrínseca ao paradigma adotado. Mas foi ficando mais fácil com o tempo.

Seja um bom ouvinte

Para efeitos de sincronia, quanto mais cedo o objeto se subscrever nos tópicos, melhor. Assim, minimiza-se o risco de um evento ser lançado sem que haja alguém para escutá-lo. No nível de abstração dos sistemas reativos, para evitar esse cenário, algumas implementações de protocolos de comunicação possuem propriedades de persistência. Casos do kafka, AMQP e MQTT.

Precedência é importante

Definir o emissor do evento é um pouco cabuloso. Em muitos momentos, há vários candidatos e a escolha não faz diferença. No entanto, existem situações que uma opção mal feita exige redesenho e retrabalho.

Por exemplo, quem deve indicar que um novo bloco deve ser criado? O próprio tabuleiro? O controlador do bloco ativo?

Padrões de Projeto

Percebi técnicas para superar as regras. Por exemplo: a regra que não permite getters pode ser contornada assim: A precisa de dados X do objeto B? Então A se inscreve em um tópico sobre X, e toda vez que X de B for alterado, B publica uma mensagem e A é informado. Reatividade no seu mais puro néctar. ❤️

Orquestrar/coreografar é a chave

Desenhar fluxos pode parecer fácil em um primeiro momento. No entanto redesenhar o fluxo de mensagem é difícil e uma tarefa constante. Rastrear as mensagens e os fluxos que os eventos geram é ainda mais difícil. De longe, o maior desafio. Quem emite o quê, quando e por quê?

Em larga escala, como uma arquitetura reativa de microsserviços, ferramentas para rastrear as mensagens são indispensáveis.

Sugiro que reproduzam o estudo com outros projetos e compartilhem as experiências na seção de comentários. Definitivamente é um estudo válido e repleto de aprendizado. Mas o quão válido no "mundo real", contudo, permanecerá uma incógnita.

Mas a diversão é certa e vocês vão apreciar para sempre.

Principalmente no jogo da cobrinha. Tinha esquecido o quão viciante era esse treco.

Nokia 3310, saudades.

Pigs don’t fly, never say die