5 dicas de arquitetura para criar componentes mais flexíveis
Nesse artigo eu gostaria de compartilhar com vocês algumas dicas que utilizamos em nossa biblioteca interna para criar componentes de UI mais flexíveis e reutilizáveis.
Os exemplos abaixo utilizam terminologias provenientes do Ember.js, porém os conceitos são agnósticos de framework. O código contém informação suficiente para demonstrar as ideias expostas nesse artigo.
Objetivos
De maneira resumida, os requisitos que a biblioteca deve atender são os seguintes:
- Produtividade
Permitir com que as equipes consigam desenvolver de maneira ágil interfaces com um alto nível de customização, minimizando reescrita de código.
- Fácil manutenção
Permitir realizar correções e evoluir as estruturas fornecidas pela biblioteca sem muitos efeitos colaterais.
- Consistência
Garantir que as estruturas possuam o mesmo visual e comportamento quando utilizadas em contextos similares, adequando-se apropriadamente à mudanças no estado da aplicação.
Abordagens
Minimizar regras de negócio
Um dos problemas mais comuns são componentes que contém regras de negócio excessivas, fazendo com que o mesmo realize tarefas que, teoricamente, estão fora do seu escopo.
Antes de implementar qualquer funcionalidade, é importante listar algumas tarefas que o componente deve ser responsável.
Imagine que estamos criando um componente de botão. Ao utilizar esse componente, eu gostaria de poder realizar as seguintes tarefas:
- Informar qual o tipo do botão — primário ou normal
- Informar o conteúdo do botão — ícones e texto
- Desabilitar e habilitar o botão
- Realizar uma ação ao clicar no botão
Tendo essa pequena listagem das tarefas, vamos identificar as diferentes partes envolvidas no processo de criação do componente e ver onde cada parte pode ser alocada.
1—O conteúdo do botão é específico de cada componente, podendo ser alocado dentro de seu próprio arquivo.
O tipo do botão é um parâmetro obrigatório, então adicionaremos uma validação caso não seja informado.
Mapear propriedades em um objeto favorece a escalabilidade do componente, caso seja necessário implementar outros estados.
2 — Vários componentes como botões e inputs possuem um estado desabilitado. As rotinas responsáveis por lidar com esse comportamento podem ser movidas para outro módulo ou uma estrutura compartilhada, evitando repetição — nós chamamos de mixin.
3 — O evento de clique também pode ser encontrado em múltiplos componentes, podendo assim ser movido para outro módulo. É importante reforçar que essa ação não deve conter nenhuma regra de negócio — somente executar o callback informado pelo desenvolvedor ao utilizar o componente.
Dessa maneira, podemos ter uma ideia dos diferentes cenários em que o componente pode ser utilizado, o que acaba contribuindo para a idealização de uma arquitetura base que beneficie sua evolução.
Separar estados reutilizáveis
Certas interações são comuns entre vários componentes, como:
- Habilitar / Desabilitar— botões, inputs
- Expandir / Recolher — collapse, dropdowns
- Exibir / Esconder — qualquer componente
Essas propriedades são geralmente utilizadas para controlar estados visuais. Sendo assim, toda a lógica relativa à esses estados pode ser movida para um mixin.
Com isso é possível reutilizar essas rotinas em lugares diferentes e manter uma nomenclatura padronizada para cada estado.
Cada rotina é responsável somente por alternar o valor de uma variável e retornar o contexto do mixin para possibilitar chaining das operações, por exemplo:
Essa abordagem pode ser incrementada com o surgimento de novas necessidades. Por exemplo, a rotina pode ser refatorada para aceitar um contexto como parâmetro e controlar tanto variáveis internas quanto externas, tornando-se mais flexível.
Abstrair funcionalidades básicas
Todo componente, independente do seu propósito, contém rotinas que devem sempre ser realizadas para garantir seu funcionamento, como verificar um callback antes de executá-lo.
Tais rotinas também podem ser movidas para um mixin próprio e reutilizadas dentro dos componentes, por exemplo:
Isso faz com que sua arquitetura se mantenha consistente, expansível e fácil de integrar com softwares e bibliotecas de terceiros.
Composição de componentes
Evite ao máximo reescrever funcionalidades. É possível criar componentes mais especializados agrupando componentes menores e sobrepondo métodos para satisfazer outras necessidades.
Por exemplo:
Componentes base: Botão, dropdown, input.Botão com dropdown => botão + dropdown
Autocomplete => input + dropdown
Select => input (readonly) + dropdown
Dessa maneira, cada componente é responsável por suas próprias tarefas, parametrização e tratamento de estado, enquanto o componente externo cuida da comunicação entre eles e das regras de negócio.
Separar responsabilidades
Ao compor componentes mais complexos, temos como prática separar os parâmetros informados entre os componentes internos.
Imagine que temos de criar um componente de select:
Internamente temos dois componentes — um input e um dropdown.
O papel principal do componente é apresentar a descrição do item selecionado para o usuário, porém ela não tem valor nenhum para a aplicação mas sim o valor do item.
Quando uma opção é selecionada, nós separamos o objeto, enviando a descrição para o input através de uma variável interna, enquanto o valor é enviado para a variável atrelada ao componente, atualizando o controller.
Esse conceito pode ser aplicado em componentes nos quais a variável atrelada deve ser transformada, como um número, autocomplete ou select.
Datepickers também podem implementar esse comportamento, removendo a máscara do valor para utilizar internamente, porém exibindo o valor mascarado para o usuário.
Os riscos dessa abordagem aumentam conforme as transformações se tornam mais complexas, seja por regras de negócio excessivas ou ter que disparar eventos conforme os valores são alterados, então recomendamos que seja feito um planejamento mais detalhado ao implementar esse conceito.
Presets vs Componentes novos
Conforme novos projetos são iniciados pode surgir a necessidade de otimizar componentes e serviços para facilitar o processo de desenvolvimento.
Essas otimizações podem ser realizadas através de presets ou componentes novos.
Presets são parâmetros. Quando informados, eles setam valores pré-definidos no componente, simplificando sua utilização. Componentes novos são, geralmente, versões mais especializadas dos componentes base.
A parte mais complexa é definir quando utilizar presets ou criar componentes novos. Nós utilizamos as seguintes diretrizes para auxiliar nessa decisão:
Quando utilizar presets
1 — Padrões de uso repetitivos
Existem situações na qual um componente é reutilizado em vários locais diferentes com a mesma parametrização. Nesses casos, é viável favorecer a utilização de presets ao invés de criar novos componentes, principalmente quando o componente base tem uma gama excessiva de parâmetros.
Os valores derivados do preset somente são setados no componente caso não tenham sido informados, mantendo sua flexibilidade.
Essa abordagem reduz o conhecimento necessário para customizar o componente e facilita a manutenção do mesmo, já que os parâmetros padrão estão definidos em um único local.
2 — Componente base é muito complexo
Quando o componente base utilizado para criar outros componentes contém muitos parâmetros, criar novos componentes pode acarretar certos problemas, como:
- Será necessário repassar a maioria —se não todos — os parâmetros do novo componente para os componentes base. Conforme novos componentes derivam a partir dele, quaisquer mudanças no componente base acarretaria uma quantidade enorme de atualizações para manter a compatibilidade, consequentemente, aumentando a incidência de bugs.
- Conforme mais componentes são criados, maior a dificuldade de documentar e memorizar as diferentes nuâncias de cada um deles, principalmente para novos desenvolvedores.
Quando criar componentes novos
1 — Extender funcionalidades
Quando for necessário extender funcionalidades, é viável criar novos componentes. Isso evita o vazamento de lógica específica de uma funcionalidade para outros componentes.
O exemplo acima utiliza o componente de botão como base, e extende seu layout para acomodar um ícone referente ao dropdown, juntamente com o componente de dropdown e seu estado atual de visibilidade.
2 — Decorando parâmetros
Uma outra motivação para criar novos componentes é a necessidade de decorar valores padrão ou controlar a disponibilidade de parâmetros.
No exemplo acima, é atribuído ao parâmetro onFocus um callback interno que seleciona o valor do campo e executa o callback informado para o componente pelo desenvolvedor.
Nesse caso, somente o parâmetro onFocus é repassado para o componente de input. Isso ajuda a controlar o escopo de certas funcionalidades e evita validações desnecessárias.
Além disso, o evento onBlur foi substituído por outro evento — onChange, executado quando o usuário informa uma data no input ou seleciona através do calendário.
Conclusão
Ao criar seus componentes, procure manter um equilíbrio entre arquitetura e utilização, facilitando a vida dos desenvolvedores.
O melhor resultado surge quando todos no grupo fazem o melhor para si mesmo e para o grupo — John Nash
Não tenha vergonha em pedir opiniões. Sempre é possível encontrar algo que possa ser melhorado.
Para aprimorar suas habilidades em engenharia de software, eu recomendo a série “Composing Software”, do Eric Elliott.
Bom, espero que tenham gostado do artigo! Aplique esses conceitos no seu dia-a-dia, encontre novas abordagens e compartilhe conosco!
Fique à vontade para entrar em contato comigo no twitter — @gcolombo_.
Obrigado!
Gostou do artigo? Compartilhe sua opinião conosco!
Não esqueça de seguir nossa página para receber todas as novidades :)