Definindo limites da Presentation e Visualization Layer com Higher-Order Components no Vue.js 2

O objetivo dessa publicação é apresentar uma técnica de composição para trabalhar com Vue.js 2 a nível de definição de limite arquitetural.

Não terá códigos nessa página, apenas em um repositório que deixei reservado para irmos analisando com calma passo a passo.

Não é sobre o jeito de fazer, é sobre uma ideia de talvez como fazer.

Sumário

Introdução
O que são Higher-Order Components (HoC)?
Presentation e Visualization Layer?
Por que pensar nessa tal arquitetura?

Entendendo o contexto

Step-by-step
Presentation Layer
Acessando a camada de UI
Higher-Order Component

A composição arquitetural

Dividir e conquistar

"Atalho"

Por fim.

Introdução

Os Higher-Order Components estendem a ideia das Higher-Order Functions da programação funcional. São padrões arquiteturais descritos para compartilhar funcionalidades comuns entre os componentes, excluindo a repetição de código.

São funções que recebem componentes (podendo também receber contextos) e concebem outros componentes.

Foram amplamente mencionados na publicação Mixins Considered Harmful de Dan Abramov, onde são apresentados como uma alternativa a composição de componentes e uma possível solução para a problematização do uso de mixins no React.

Por mais que o título do tópico pareça instigante a ponto de tentarmos adivinhar o que significa, a ideia por trás é um pouco mais complexa.

Esses conceitos foram mencionados no livro Clean Architecture: A Craftsman’s Guide to Software Structure and Design de Robert C. Martin (a.k.a Uncle Bob) tendo por objetivo a definição de limites arquiteturais.

1.0 The Clean Architecture — https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

A imagem acima é uma representação ilustrativa da Clean Architecture de Uncle Bob.

Resumidamente, os círculos de dentro são as camadas mais importantes da nossa aplicação, centralizando toda a regra de negócio do sistema. A divisão de um circulo para outro é o denominado limite arquitetural.

Nesta publicação iremos fazer referencia a terceira e quarta camada da figura. A Visualization no contexto faz referência a UI, que se encontra na camada mais periférica da arquitetura.

Quantos de nós ja não tivemos que realizar refatorações (não no sentido Conventional Commit da palavra) em algumas de nossas regras de negócio, quando o responsável de UI altera algo na interface da aplicação que trabalhamos?

Por esse e outros motivos mencionados no livro de Bob, realizamos a dissociação de coisas que mudam com frequência de coisas que não mudam com tanta frequência. Separamos coisas que mudam por motivos diferentes (afinal, os princípios do SOLID não estão somente na menor granularidade do nosso app né).

Entendendo o contexto

Iremos aplicar os exemplos em um proto app de chat (a.k.a Hello World de hoje em dia).

Antes de continuarmos, teremos que proxiar algumas regras dos famosos princípios da Clean Architecture e Dependency Inversion.

  1. Nossa suposta Presentation Layer contém a regra de negócio da aplicação, pois o foco deste não é apresentar como implementar a Clean Architecture por inteira no projeto. Suponhamos que temos outras camadas internas contendo nossas regras de domínio, e que estão sendo importadas na Presentation e sendo adaptadas para o uso.
  2. Se observarmos, o fluxo de dependências arquitetural aponta na direção correta, tendo em vista que a Visualization Layer é de camada mais baixo nível em ótica da Presentation Layer. Porém não vemos o princípio de Dependecy Inversion (DIP) ocorrendo. Isso se da pelo mesmo motivo do tópico acima. Mesmo não sendo o objetivo, gostaria de deixar claro que um projeto na vida real necessitaria sim de todas essas outras particularidades.

Reconhecendo tais regras antes do contexto, a visão de fora do que iremos seguir:

2.0 Visão geral do projeto

Nosso módulo @presentation/ChatConversation contém o intermédio do que é importante na nossa aplicação — regra de negócio.

O HoC (Higher-Order Component) é responsável por compor nossos componentes "burros", de uma maneira que ganhamos tanto com desacoplamento quanto com reaproveitamento de código inteligente.

No mais periférico temos os componentes propriamente ditos. O box @visualization/Component contém a UI.

Importante salientar, que nesse modelo utilizamos a tag script dos nossos componentes de UI apenas como adapters ou consumidores de conteúdo das camadas de maior nível. Fantástico!

Por fim e não menos importante, temos o famigerado App.vue como orquestrador de tudo. Sim, eu sei, estou colocando a montagem da aplicação inteira no componente entry point do projeto… Porém, para nosso exemplo não necessitamos de uma estrutura mais organizada.

Volto a ressaltar, em um projeto na vida real não faríamos o que o parágrafo acima expõe.

Antes de iniciarmos, o source da publicação se encontra nesse repositório:

Step-by-step

Dentro da pasta presentation (que está em src), se encontra um arquivo chamado chat-conversation.js.

Se observarmos, teremos uma suposta e abstrata "apresentação" para a área de conversação no chat. Dentro desse módulo, podemos exibir todas as mensagens ou salvar uma mensagem em nosso bucket de mensagens. É isso que esse componente deve fazer.

Por mais que nossa suposta "regra de negócio" seja extremamente simples no exemplo dado, ganhamos com desacoplamento de coisas que mudam por motivos diferentes — coisas importantes das não tão importantes.

Precisamos de uma interface para visualizar a implementação desta regra de negócio específica. Que entre a Visualization Layer!

Para esse exemplo simples, optei por um componente que encapsule as mensagens que estão transitando no chat em uma tag section, além de outro responsável por capturar a mensagem que o usuário digita.

Ah, e não menos importante, um componente orquestrador que executará de maneira extremamente declarativa, chamadas a métodos provenientes de nossa camada de apresentação. Isso fica a gosto.

Podemos observar esse conjunto de funcionalidades dentro da pasta components: os compontes "send" e "messages".

Se você observar as estruturas acima citadas de acordo com a ordem que as coloquei, pode ser que desperte certa dúvida em relação de onde vem a prop receiveData, do componente messages.

Ai que entramos em um dos cernes dessa publicação.

Podemos observar o source usado para tal na pasta shared dentro de components. Decidi distribuí-lo nesse limite apenas para dar proximidade a utilidade dele no exemplo.

Observando o arquivo, vemos que temos uma dependência direta com a camada que está acima em nossa arquitetura, como indicava o diagrama.

Também temos uma função que recebe alguns parâmetros que irão compor nosso componente — o container da operação.

Sintetizadamente recebemos o componente que será composto propriamente dito, um nome arbitrário e uma propriedade chamada fetch. Essa é a responsável por abrir uma brecha para escolhermos as informações de nossa apresentação que iremos dedicar ao componente — se não ficou tão claro, aguarde um pouco que já explico o outro lado da moeda.

Retornamos um componente Vue. Esse componente é montado por algumas informações que passamos na assinatura da função.

Repare a regra de dependência que crio entre o HoC e o componente composto, ao passar como props as informações que foram inicialmente atribuídas ao HoC pela propriedade data. Isso, como o evento de enviar mensagem que disparo para o orquestrador mencionado acima, são a gosto também. Apenas para continuar a lógica de dependência, nada tão relevante.

Algo importante para o funcionamento desse modelo onde oriento declarativamente o orquestrador acessar propriedades da Presentation Layer por evento, está na linha 11.

A composição arquitetural

Começamos permitindo a distruibuição de nossos componentes no arquivo index.js, contido em cada pasta que contém o código do componente Vue.

Se observarmos em src/components/messages/messages/index.js por exemplo, podemos ver que monto o componente que imprime as mensagens em tela.

Na propriedade fetch (o outro lado da moeda) apenas retorno o que realmente preciso da minha camada de apresentação, reduzindo dependências que não são necessárias — dependemos apenas de código que precisamos.

Nesse caso, por exemplo, eu "isolo" a propriedade exibeAllMessages. Essa propriedade é uma função getter responsável por retornar as mensagens de nosso bucket messagesList, encapsulado a nível de arquivo.

Nesse ponto de desacoplamento, abrimos margem para aplicar um padrão de projeto que nos ajuda com a testabilidade do código, chamado Humble Object. Falaremos disso em outro episódio.

Alguns talvez devem estar se perguntando o por que importei o arquivo de apresentação do chat no source do HoC. Apenas decidi pela ótica de "dedupação de código desnecessário; o HoC está contido dentro de shared, que por sua vez se encontra dentro dessa composição de visualização de componentes. Talvez isso seja a maior referência pra essa dependência.

Dividir e conquistar

Dando um passo pra trás, respirando fundo, olhando novamente o diagrama do projeto, podemos tirar algumas conclusões.

Parece-me que a maioria dos princípios que grandes mestres deixam a nós, se volta para o âmbito das menores granularidades. Dividir e conquistar.

Falamos hoje em microservices, Single Responsability Principle (SRP), Bounded Context (DDD), Composition (PF) entre inúmeros outros conceitos e princípios que nos ensinam a pegar um grande problema e fracioná-lo em menores. Afinal, quando temos apenas uma coisa pra fazer, as mais mirabolantes ideias — tanto as inteligentes quanto as não inteligentes — são registradas. Dividir e conquistar.

Nesse modelo de composição não é diferente. Ganhamos com desacoplamento, definição de limites e reutilização de código inteligente.

Essa última expressão, reutilização de código inteligente, uma grande armadilha que quem nos pediu tal técnica, não nos informou.

Robert C. Martin cita sobre sobre a repetição de código acidental. Acredito que, seguindo os princípios de técnicas como o SOLID para POO, Composition e Currying para PF, dentre vários outros, conseguimos transformar o nosso problema em um menor. Fazendo isso, talvez aquele cheiro de código que pode ser dedupado fique ao alcance de nossas mãos.

"Atalho"

Como dizia um professor com qual tive aula na faculdade, "após aprender o grosso, vamos pelo atalho".

A dependência acima facilita a criação de Higher-Order Components no Vue.js.

O ganho inicial seria não precisar arquitetar completamente como que o HoC irá servir aos limites de componentes pelos quais está sendo compartilhado.

Porém, até aqui ja sabemos a teoria e prática, podendo então remover uma dependência desnecessária de nosso projeto.

Por fim.

Nesse modelo, ganhamos em desacoplamento a nível arquitetural (por mais que o exemplo da publicação seja enxuto).

Abrimos margem para uma taxa de coverage maior em nossos testes, ganhando com a opção de testar e validar nossa regra de negócio de maneira isolada.

Agora, sempre que o designer alterar a UI do projeto, nossa regra de negócio está isolada dessas ações, removendo, mais uma vez, coisas que mudam por motivos diferentes.

Referências

https://linkedin.com/pedromoraisf

I write about Architecture and Software Engineering. https://github.com/pedromoraisf 🇧🇷

I write about Architecture and Software Engineering. https://github.com/pedromoraisf 🇧🇷