E Ottaviani AragãoBlog Projetos Sobre Mim

Arquitetura e Camadas de abstração — Uma proposta para Front End

Se você já está há algum tempo desenvolvendo sistemas no lado client-side, você já deve ter percebido que há bastante material falando sobre arquiteturas no lado back-end. Um equívoco que acredito eu cometi por muito tempo foi tentar trazer estes conceitos ou abstrações para o front-end, confesso que o resultado nunca foi muito… satisfatório.

Photo by Kenny Eliason on Unsplash

Alguns erros que cometi…

Tentei trazer alguns conceitos que faziam muito sentido no back-end e que faziam muito sentido para o paradigma mais utilizado em linguagens como Java, C#, PHP, que era o paradigma orientado à objetos. O problema é que o forte do Javascript sempre foi o modelo mais funcional embora tenha elementos de herança e propriedades de sistemas que trabalham com objetos.

Então tentei de maneira muito pouco eficiente trazer para o front conceitos como Design Patterns e o padrão MVC. Não estou dizendo que eles não servem para o front, porém eu percebi em várias situações que eles precisavam ser traduzidos ou adaptados porque não funcionavam de maneira tão elegante no front.

Na questão dos design patterns, se você utilizasse o Javascript de maneira mais funcional, algumas soluções perdiam o propósito, pois resolviam certos problemas inerentes ao paradigma orientado à objetos. Outros, haviam formas mais simples e diretas de resolver.

Na questão do MVC bati muito a cabeça ao tentar encaixar qual parte de uma aplicação UI era uma controller, qual era a view e qual era a model. Para quem trabalha com o DOM, seu html pode ser a view, ao mesmo tempo que pode servir para te dar informação de dados e suas controladoras e componentes acabam se misturando até o ponto de você acopla-los de maneira a seguir o padrão mas perdendo coesão.

Ao tentar utilizar os conceitos originários para soluções de problemas de Backend no Front, me parecia que estava tentando fazer um martelo se comportar como uma parafusadeira.

No final das contas a gente usa muitos conceitos de design patterns, MVC, mas eu vejo que no Front fazemos isso em um contexto mais micro ou específico. Então eu comecei a procurar maneiras de se pensar em uma arquitetura padrão para uma aplicação no Front.

Clean Architecture e a minha visão da Arquitetura Padrão no Front

A gente aprende com os erros, ou assim se espera… E portanto, assim que li sobre Clean Architecture e comecei a consumir o conteúdo através de vídeos mostrando suas implementações, me veio na cabeça aquele sentimento: Já passei por isso antes, de tentar migrar um modelo back-end para o front, não vou cometer o mesmo erro de novo.

Isso não significa que eu ignoro completamente Clean Architecture, muito pelo contrário, eu só sei que não vale a pena trazer para o front de maneira que foi concebido principalmente porque entendi quais problemas o Uncle Bob queria resolver e conversa com as mesmas preocupações que eu tenho no front, e portanto a visão de Clean Architecture mais abriu minha mente do que fechou.

Ao invés de tentar migrar uma arquitetura de um ambiente que tem suas idiossincrasias e tentar fazer funcionar no ambiente Client Side, o melhor que eu consegui fazer foi observar o que eu fazia no dia a dia que eu acreditava que simplificava as soluções e que funcionavam de maneira elegante e direta, também absorvendo o que a comunidade de front compartilhava.

Nessa observação sobre repetição, na criação de projetos do zero ou mesmo nas refatorações, eu percebi que um um padrão começava a emergir.

Dei um spoiler ali em cima na imagem, mas parte da visão é com relação a quais são as classes de problemas que existem e a outra parte é como elas se relacionam.

A minha visão é só uma mistura de tudo o que venho aprendendo e muitas delas vocês já devem saber, nada de novo aqui, e esse é para mim o principal na hora de pensar em um padrão, deveria ser algo comum, que as pessoas já conhecem ou já viram, não é tentar revolucionar e reinventar a roda.

Classes de problemas no Front End

Como mostrado no gráfico, existe uma classe de problemas que vejo de forma recorrente nas aplicações Front, separadas e ilustradas nas camadas abaixo:

Estou pensando de maneira agnóstica. Assim como o Uncle Bob, vejo todos os frameworks do front como coadjuvantes em um sistema.

UI System 💅

A primeira delas é a que eu chamo de UI System. Pensando em aplicações que podem escalar para vários times, um ponto que considero crucial é a consistencia visual entre as aplicações.

Quantas vezes vi ou trabalhei em aplicações diferentes da mesma marca, desenvolvidas por times diferentes, mas que possuem inconsistencias de layout, botões e elementos diferentes entre duas aplicações, porque em uma aplicação o estilo dos botões foi atualizado e em outra aplicação o estilo ainda estava antigo e não havia uma integração entre eles ou sequer uma forma de consumir esse estilo a fim de replicar. No pior dos casos o que eu vi foi um copy & paste de um Css de uma aplicação para a outra.

Além da consistência, existe o fator de reutilização de código. Não seria maravilhoso começar uma aplicação nova, de uma marca que já possui um design system e você já pode desenvolver sem se preocupar em recriar os elementos fundamentais?

Então muitas pessoas já desenvolvem nesse modelo hoje, não é uma novidade, mas a minha visão é que você não precisa necessariamente criar um design system separado, ou mesmo um design system, se sua aplicação for muito simples, porém, você precisa ter uma camada onde você endereça esses padrões visuais em algum lugar na sua aplicação, de maneira que possa no futuro, se essa aplicação crescer, substituí-la por um design system.

Hoje os Design Systems acabam acoplando essa padronização visual feita no Css, com os componentes e suas funcionalidades, sou um crítico com relação à esse modelo, mas… es lo que hay.

Eu chamo de UI System, porque é um sistema de UI que não necessariamente precisa ser um Design System, parece difícil né? O que eu estou tentando dizer é que o UI System é o bootstrap.css da sua aplicação, contendo:

  • Variáveis
  • Mixins
  • Classes globais
  • Tipografia ( Títulos, texto corrido, links, espaçamentos )
  • Cores

E quaisquer outros elementos que são fundamentais. Essa abstração é totalmente copiada da idéia de "frameworks Css" que apareceram com o intuito de reutilizar Css de maneira mais genérica.

Pode ser que o UI System da sua aplicação seja um Bootstrap, Foundation, Bulma… ou pode ser que seja um sistema próprio, mas a idéia é que consiga desenvolver de forma apartada e se algum dia precisar, poderia desacoplar da aplicação e importar de maneira simples em qualquer outro sistema e reutilizar, independente do framework que esteja usando.

Os frameworks Css tem como propósito serem mais generalistas, e portanto tem mais elementos e componentes do que você talvez precisasse. Ao criar um sistema próprietário você pode e deve simplificar, colocando apenas elementos que sabe que a probabilidade de ser reutilizada é alta.

Vou ser bem polêmico agora, mas … é por uma boa razão. Pelo mesmo motivo que eu tinha a tendência a trazer conceitos do back no front sem muito sucesso, vejo acontecendo no cenário de Front End, no caso, a tentativa de trazer o conceito de Microservices para o formato do Front, chamando de Micro Front-ends, ao tentar resolver o problema gestão de múltiplos times e múltiplas stacks.

Te digo, que se você tiver um UI System desacoplado, pronto para ser reutilizado por outra aplicação, outros times poderiam iniciar suas aplicações em outros repositórios, usando a stack que quiserem e mantendo a identidade visual da marca, importando seu UI System como se fosse um Bootstrap, e criando rotas que respondem à estas aplicações feitas com o framework preferido daquele time... já pensou nisso?

Um arquivo .css é compatível com qualquer aplicação Web…

Componentes 🧩

Uma vez definido o estilo padrão da aplicação, componentes nada mais são do que a combinação entre as partes para se formar o todo, como se fossem blocos. Você talvez tenha conhecido esse conceito através de Frameworks populares como o React, porém, este conceito é antigo, desde o primórdios da Internet. Esta é basicamente a definição da junção de um conjunto de elementos HTML.

Você, mesmo sem nenhum framework consegue criar uma página inteira apenas combinando os elementos html e criando seções e partes maiores até chegar no nível de uma página completa.

Mas há sim, uma diferenciação com relação à forma como viamos componentes antigamente no Front trazida pelos frameworks. É o fato de que agora olhamos para um componente não só como um combinado de elementos HTML, mas trazendo junto, de maneira acoplada, o Javascript e o Css específico para aquele componente.

Então agora, na minha definição aqui, componente é um compilado de Css, Html, Js que em seu contexto resolve um determinado problema. E este componente pode ser composto de vários outros componentes filhos.

Eu compartilho da mesma preocupação do grande mestre Uncle Bob, que é a dor de quando de bate o olho na aplicação e percebe que a primeira coisa que você vê é o framework. Porém eu não acredito que seja possível resolver isso de maneira tão fácil no front. Eu vejo que no front existe uma tendência a acoplar demais as coisas, talvez por sua natureza intrínsica.

Mas eu acredito que a gente pode diminuir o acoplamento entre o Framework e nossa aplicação a fim de delegar apenas pro framework, aquilo que o framework faz bem, e é nesta parte de Componentes que eu acho que os Frameworks brilham e é apenas nessa camada que deveriam estar mais focados, ouvindo eventos do usuário e atualizando a UI.

Services 📞

Existe uma infinidade de definições para serviços, e eu para ser sincero sou incapaz de entender o que esperar de um serviço em cada aplicação, seja ela back-end ou front, mas eu quero ser ousado e definir aqui da minha cabeça, dada a minha experiência na prática, como eu vejo um serviço.

Serviço no contexto do Front End na minha visão é uma caixa preta, uma função que recebe parâmetros e retorna uma Future/Promise, e portanto é por definição sempre assíncrono. A tarefa dos serviços é encapsular toda a lógica e problemas relacionados à :

  • Fazer uma requisição para uma API, encapsulando e utilizando uma interface para a requisição: fetch , axios etc.
  • Fazer composição entre múltiplas requisições ou múltiplos serviços
  • Massagear / Tratar os dados de retorno de maneira padronizada

Nessa camada eu costumo resolver todos os tipos de requisição externa que existe na minha aplicação, e dentre estes problemas que ele endereça que listei acima, acabo delegando para essa camada, chamadas de Tracking e também BFF's que às vezes precisam ser feitas no lado do Front.

Em muitos projetos que trabalhei as api's principalmente que estavam no modelo REST pulverizavam a informação em outras situações traziam os dados em um formato que não era o ideal no contexto da UI. Já sei o que vai pensar… e não, A SOLUÇÃO NÃO É IMPLEMENTAR UM GRAPHQL.

Um parenteses aqui, preciso endereçar essa dor que eu tenho…Talvez se você trabalha em uma empresa do tamanho do Facebook, então TALVEZ o GraphQL faça sentido para você…

Mas sendo 100% sincero, não houve uma única vez na vida que uma simples chamada à uma API formatada, em uma simples conversa com um back-end não resolvesse. E sim, já trabalhei em sistemas ultra simples e ultra complexos também.

Não se engane achando que implementar GraphQL na sua aplicação irá resolver problemas de múltiplas requisições sem te cobrar um preço por isso.

E o preço que ele cobra eu considero alto:

  • É mais uma stack que seu desenvolvedor iniciante precisará aprender sem a menor necessidade.
  • É mais uma complexidade na sua aplicação que o próprio back-end terá de resolver também.
  • É mais um ponto crítico que cria na sua aplicação podendo impactar na segurança ou na manutenção.

Na maioria das vezes um BFF resolve o problema, mas há situações que o back-end não conseguirá criar um BFF para você, então uma saída é usarmos composições de funções na camada de Serviços, utilizando do poder das Promises e das funções para juntar os dados e devolver em um formato ideal para a nossa UI.

No final, acamada de serviços na minha visão é apenas um conjunto de funções as quais recebem argumentos e retornam uma promise, como eu comentei anteriormente, compatível com qualquer sistema Web e aqui, não vejo a necessidade de ter acoplamento com nenhum Framework.

Portanto, coisas tipo : react-fetch-hook e variantes não fazem sentido nessa proposta…

Entities 👤

Eu acredito que já tenha passado por esse problema antes, sabe quando você bate na api de algum serviço específico do Google, tipo o Google Maps, ou api's do Facebook, ou Instagram, e ela vem em uma estrutura de dados toda bagunçada?

Em algumas propriedades você tem que fazer algum tipo de formatação, mas no geral o que atrapallha é a organização dessa estrutura mesmo. Eu costumava resolver esses problemas na camada de serviço. Mas eu comecei a perceber duas coisas.

  1. Os serviços começaram a ficar um pouco mais complexos, e acabava desempenhando duas tarefas, uma de fazer a integração entre as api's e a segunda tarefa de fazer a organização dessas estruturas de dados.
  2. Existiam casos onde eu precisava reutilizar aquela função que organizava os dados de dentro do serviço, e para isso eu fazia o que todos nós fazemos em algum momento da vida, movemos para uma pasta helpers/utils . Quem nunca?

O problema de ficar migrando tudo o que reutiliza para uma pasta utils é que essa pasta possui um nome muito genérico, fica dificil de antemão saber, caso nunca tenha mexido no projeto, que tal função estaria nessa pasta. E indo para esse caminho de jogar tudo o que se reutiliza nessa pasta, você já consegue prever que essa pasta irá crescer indefinidamente, portanto vai escalar mal.

Se este é um tipo de problema recorrente então ele pertence à uma classe de problemas que podem ser abstraídos.

Essa abstração é uma Entity. Na minha releitura para o front, é uma função que recebe um parametro e retorna uma estrutura de dados que diz respeito à uma classe de dados, uma entidade. Ficou muito abstrato?

Pensa que você faz uma chamada de um serviço que retorna um produto. Um produto tem um preço. Aqui eu posso abstrair pensando em duas entidades: ProdutoPreço.

/** @type {Price} */
export const Price = (
price = Number()
) => ({
price : {
raw : price,
value : formatters.price( price ),
}
})
/** @type {Product} */
export const Product = ({
    id = Number(),
title = String(),
slug = String(),
description = String(),
image = String(),
price = Number()
}) => ({
id,
title,
slug,
description,
image,
price : Price( price )
})

Os parâmetros são ao mesmo tempo parâmetros default, que de certa forma garantem o valor default caso falte no retorno da api, ao mesmo tempo que serve de documentação para saber quais propriedades e tipos aquela entidade recebe e devolve.

Lembra um pouco a estrutura de interface se você utiliza em uma linguagem orientada a objetos ou typescript, porém aqui é de fato uma estrutura que possui implementação.

Dessa forma, a gente pode utilizar essa entidade para formatar uma estrutura de dados proveniente de um retorno de um serviço.

Uma coisa que percebi usando essa abstração é que deleguei para as entidades algumas tarefas que, pensando bem, fizeram muito sentido ser delegadas. Geralmente eu fazia conversões de preço direto no componente importando algum formatador. Mas de fato, ficava repetitivo, em todos os componentes eu chamava um formatador.

Dessa forma, eu conseguia, olhando para minha pasta entities saber sobre todas as estruturas de dados que existem na aplicação, e elas podem ser utilizadas tanto na entrada de parametros dos serviços, caso você precise formatá-los, ou na saída após resolver uma promise de uma chamada de api, formatando para a aplicação consumir através dos componentes.

Além disso, por ter os parâmetros default, eu resolvia alguns casos onde minha aplicação quebrava porque faltava uma propriedade na resposta da API.

Por retornar um objeto, é possível também fazer composição de entidades usando spread operator, ou seja, encaixou como uma luva:

/** @type {ProductDetail} */
export const ProductDetail = ({
id,
title,
product
}) => ({
id,
title,
...Product( product )
})

E assim terminamos a camada Entity, você percebe aqui, como nas demais com excessão de Components, que é puro Javascript, aqui o framework não entra, o que não te impede de usar bibliotecas que resolvam algum problema de transformação de dados.

Stores 🕋

Uma coisa que sempre achei muito over engineering foi o Redux. De cara eu já sabia que existia uma complexidade desnecessária no Design da solução, até escrevi alguns posts mas como eu não tenho uma relevância grande na comundade minhas palavras se perdiam pelo vento.

Mas não era por uma resistência ao que era novo, mas sim um olhar mais científico, tentando refutar certas idéias mas especialmente absorvendo outras. E o que eu aprendi com o conceito de stores, tanto com o Redux quanto com o Flux, é que era uma maneira de organizar o fluxo de dados e principalmente a comunicação entre dois componentes satélites, além de facilitar a persistência de dados.

Na minha concepção aqui dessa nossa arquitetura, o principal objetivo das stores é fazer o relacionamento entre os componentes da aplicação, e ser responsável por armazenar e orquestrar as regras da aplicação.

Então, pense em uma tela onde existe um catalogo de produtos, voce tem a opção de adicioná-los no carrinho diretamente do componente de produto, ao fazer isso, o ícone de carrinho atualiza o número de itens no carrinho no header, e o seu usuário só pode adicionar até 5 itens, não mais que isso.

Antigamente eu concentrava essas regras em um componente pai, que servia de controladora e fazia esse controle. Porém hoje, eu delego essas regras pra store.

Então, no caso acima, o componente do produto dispararia uma ação para a store, repassando o id do produto que eu adicionei. Um outro componente header, se inscreveu para ouvir os updates da store para reagir às ações de adição e remoção de itens, atualizando seu número conforme a quantidade de itens adicionados resgatados da store, e um outro componente modal de "warning" também se inscreve para ouvir os updates da store e abre assim que o número de itens adicionados ultrapassa de 5.

Mas claro essa regra é definida dentro da store, na tentativa de adicionar mais um item em uma determinada ação, lá na store é brecada essa tentativa, e alterada alguma propriedade do estado da aplicação indicando que o limite de adição ao carrinho foi alcançado, também verificando o tipo de usuário que tentou a operação. O componente do modal nem precisa pensar em calcular nada, basta apenas cuidar da apresentação, ao aparecer com alguma classe open css ou alguma lógica que faça a renderização do componente na tela.

Qual a vantagem de centralizar estas regras na Store? Bom, você consegue testar todo o fluxo e regras da aplicação sem que a tela tenha sido sequer implementada na interface visual, isso porque a store é apenas uma máquina de estados que implementa o padrão Observer, executando funções pré-definidas de acordo com uma ação específica e notificando à todos que foram registrados nela. E isso é simplesmente genial.

O ideal é que essa Store também fosse agnóstica, não fosse específica de nenhum framework, mas que pudesse trabalhar em conjunto com qualquer framework.

Existia uma diferença de conceitos entre Redux e Flux no sentido que o Redux pregava uma "única fonte de verdade" ou seja, um lugar concentrando o estado da aplicação inteira e isso óbviamente não escalava bem, principalmente para um produto com muitas telas, virava rapidamente um monolíto enorme que fortalecia o acoplamento entre as telas, além de toda a complexidade inerente do sistema Redux que tornava tudo muito burocrático.

No final, percebi que o ideal é ter 1 Store, porém com as actions / usecases separadas, assim, você sempre terá no contexto da sua página 1 store, porém terá as actions/reducers no contexto da aplicação global, então sua store é uma composição de todas as ações que podem acontecer na sua aplicação no nível global.

Helpers & Utils 🧰

Talvez essa "abstração" seja a mais utilizada e a mais fácil de compreender dentre todas as outras. Essa é a parte onde entram as funções mais genéricas que podem ter uma funcionalidade genérica mas que faz mais sentido dentro da sua aplicação, e portanto, são uma alternativa aos módulos npm que podem não se encaixar para a sua solução.

Às vezes eu coloco funções que reutilizo como throttle , debounce que poderiam ser importadas via npm, porém as vezes as soluções são tão básicas e simples de poucas linhas que faz mais sentido ter dentro do projeto do que importar, e pode contribuir pra deixar a instalação do projeto mais rápida, porque é menos um módulo pra baixar.

Uma coisa que eu acho que vale a pena falar aqui é que ainda que seja algo muito genérico, é preciso ter cuidado para não criar um monstro.

Existem certos tipos de problemas que podem ser organizados como sendo de uma mesma classe de problemas, por exemplo, máscaras de validação de formulário ou formatação de strings e números. Aqui, vale a pena pensar se não é interessante organizar em pastas de acordo com essa classe de prolemas para poder se achar, porque ter uma pasta utils e dentro dela 200 arquivos javascript… não é algo muito gostoso de se ver…

Minha sugestão é criar lá uma pasta utils/formatters , utils/masks e asssim por diante. Geralmente coloco nessa pasta tudo aquilo que não cabe nas outras abstrações.

Conclusão

Bom, a idéia aqui era separar as classes de problemas que acontecem de maneira recorrente ao desenvolver uma aplicação web no Front End, de maneira análoga à maneira como foram pensados os Design Patterns, quis identificar quais são estas classes e categoriza-las, definindo seus limites, seus propósitos e papéis.

Estou de acordo com a visão do Uncle Bob com relação à empurrar para os limites da aplicação detalhes de implementação evitando o forte acoplamento.

No mundo front-end isso é um desafio particularmente grande, no final eu quis limitar ao máximo a influência que o Framework exerce na aplicação, porque essa é a minha maior dor como desenvolvedor Front-end/Javascript, me dói muito ver que damos tanto valor aos frameworks sendo que estes deveriam ser apenas um detalhe em qualquer aplicação Web.

Trabalhando muito tempo em aplicações vanila isso sempre me pareceu verdade, e agora refletindo sobre o tema de arquitetura isso fica cada vez mais claro na minha mente.

Em um próximo post gostaria de mostrar e compartilhar um exemplo de aplicação simples para botar em prática estes conceitos. Uma parte que penso ser importante nessa aplicação prática é a estrutura de pastas. De fato, como costumam dizer, a arquitetura não diz respeito à estrutura de pastas ou como organiza os arquivos, porém eu acredito que a estrutura de pastas é parte fundamental para que sua arquitetura seja bem sucedida.

Eu duvido muito que consiga implementar uma sistema que escala bem, mantendo todos os arquivos juntos em uma única pasta delegando a responsabilidade da facilidade de manutenção apenas para a arquitetura.


Arquitetura e Camadas de abstração — Uma proposta para Front End was originally published in Carreira Solo on Medium, where people are continuing the conversation by highlighting and responding to this story.