loading...
Cover image for Barrel Pattern: import de módulos de forma elegante em JavaScript

Barrel Pattern: import de módulos de forma elegante em JavaScript

exploitmik profile image Mikéias Oliveira Updated on ・6 min read

Introdução

Barrels são formas de acumular exportações de vários módulos em um arquivo base. Em resumo, são arquivos que re-exportam partes selecionadas de seu módulo. É uma técnica usada por muitas aplicações, inclusive Angular, React e Vue.

Provavelmente você já se deparou com importações neste formato:

// Vue
import { defineComponent, reactive, computed } from 'vue';

// Angular
import { Router, Route, ActivatedRoute } from '@angular/router';
Enter fullscreen mode Exit fullscreen mode

Como Angular e Vue utilizam monorepo, ou seja, temos uma separação da aplicação em packages com responsabilidades definidas, é conveniente que as partes importantes desses packages sejam agrupados e exportados em um arquivo base que arroga a tarefa de ser o ponto central de acesso as partes daquele módulo. Também vale ressaltar que esta abordagem não se restringe a monorepo.


Importação de Módulos

Com a massiva atualização que o JavaScript recebeu em 2015 (aka ES6), dentre as novas funcionalidades recebemos o ESModules: um sistema oficial de módulos para o JavaScript. Havia alternativas como CommonJS e RequireJS, mas agora temos o suporte a módulos nativamente. Se deseja entender um pouco mais a fundo sobre módulos, indico o capítulo do livro do Dr. Axel Rauschmayer: Impatient Programmers - Modules. Ressalva: também é possível utilizar Barrel Pattern com CommonJS, mas me restringirei a explicá-lo com módulos nativos.


O contexto

Agora imaginemos um contexto: se estamos trabalhando com um Design System que se utiliza da filosofia Atomic Design, logo mais chegaremos a uma situação semelhante em diversas partes da nossa aplicação:

// Navbar.vue

import Menu from './components/menu/Menu';
import MenuList from './components/menu/MenuList';
import MenuItem from './components/menu/MenuItem';
import MenuLink from './components/menu/MenuLink';
import Select from './components/select/Select';
import SelectList from './components/select/SelectList';
import SelectOption from './components/select/SelectOption';
Enter fullscreen mode Exit fullscreen mode

Percebemos que estamos importando 4 componentes da pasta menu e 3 da pasta select, repetindo o caminho de diretórios e alterando apenas o nome do componente. Certamente, se pudéssemos melhorar o código acima por um mais elegante seria interessante. Vejamos um exemplo com Barrel:

// Navbar.vue

import {
  Menu,
  MenuList,
  MenuItem,
  MenuLink
} from './components/menu';

import {
  Select,
  SelectList,
  SelectOption
} from './components/select';
Enter fullscreen mode Exit fullscreen mode

Agora temos uma sintaxe mais enxuta, não precisando repetir o nome do componente devido a desestruturação do objeto que o arquivo barrel de cada módulo exporta. Porém, ainda podemos melhorar.

Caso não tenha familiaridade com o conceito de desestruturação de objetos e arrays, indico como referência: JavaScript.info - Destructuring Assignment.


A abordagem

Como já vimos, é possível desestruturar um objeto exportado e extrair somente o que nos é necessário. Então, vamos entender como funciona um arquivo barrel (o arquivo que exporta a API pública do módulo).

Vejamos como o Angular exporta partes do package @angular/router:

Analise na íntegra e veja que existe um arquivo index.ts exportando partes de router. A escolha do nome do arquivo não é por acaso. No sistema de módulos, quando referenciamos uma pasta, é buscado inicialmente por um arquivo index.

Levando em conta, assim ficaria nossa pasta components em uma aplicação Vue:

components
├── menu
│   │── Menu.vue
│   │── MenuList.vue
│   │── MenuItem.vue
│   │── MenuLink.vue
│   └── index.js <-- arquivo barrel
├── select
│   │── Select.vue
│   │── SelectList.vue
│   │── SelectOption.vue
│   └── index.js <-- arquivo barrel
Enter fullscreen mode Exit fullscreen mode

Agora exportamos os componentes que desejamos:

// components/menu/index.js

export { Menu } from './Menu.vue';
export { MenuList } from './MenuList.vue';
export { MenuItem } from './MenuItem.vue';
export { MenuLink } from './MenuLink.vue';
Enter fullscreen mode Exit fullscreen mode

Pronto. Ao acessarmos components/menu, o arquivo barrel (index.js) será chamado e retornará um merge dos objetos com nossos componentes, e então poderemos usar a desestruturação para extrair o que realmente precisamos:

// Navbar.vue

import { Menu, MenuList } from './components/menu';
Enter fullscreen mode Exit fullscreen mode

Melhorando a abordagem

A abordagem acima é bem interessante, porém é bom ficar atento para não exagerar no uso de barrels. Nós não queremos ter muitos arquivos, pois isso é contraproducente e geralmente leva a problemas de dependência circular que às vezes podem ser bem complicados de resolver.

O recomendado é que cada módulo tenha um arquivo barrel, delegando ao mesmo a responsabilidade de exportar as suas partes convenientes. Deve concentrar a API pública e ser responsável por encapsular o acesso de suas partes. Caso a localização de uma pasta interna seja alterada, aqueles que utilizam seu módulo não devem ser afetados.

Sabendo disso, podemos colocar um arquivo barrel na raiz da pasta components e exportar suas partes internas de forma concentrada:

components
├── index.js
├── menu
│   └── ...
├── select
│   └── ...
Enter fullscreen mode Exit fullscreen mode

Agora podemos exportar de duas formas:

Individual, sendo assim, não é necessário um arquivo barrel em cada pasta interna referente a um componente (recomendado):

// components/index.js

export { Menu } from './menu/Menu.vue';
export { MenuList } from './menu/MenuList.vue';
export { MenuItem } from './menu/MenuItem.vue';
export { MenuLink } from './menu/MenuLink.vue';

export { Select } from './select/Select.vue';
export { SelectList } from './select/SelectList.vue';
export { SelectOption } from './select/SelectOption.vue';
Enter fullscreen mode Exit fullscreen mode

Ou reaproveitando os arquivos barrels e re-exportando no arquivo barrel base do módulo:

// components/index.js

export * from './menu';
export * from './select';
Enter fullscreen mode Exit fullscreen mode

No final das contas teremos o seguinte resultado:

// Navbar.vue

import { Menu, MenuList, Select, SelectList } from './components';
Enter fullscreen mode Exit fullscreen mode

Conseguimos acessar todos os nossos componentes apenas referenciando a pasta components. Não precisamos saber quais componentes estão lá, nem lembrar das subpastas que exportam algo. Tudo o que está acessível para importar encontra-se no arquivo barrel base (API pública do módulo).

Dica: se você usa o VSCode, apertando Ctrl+Space dentro das chaves lhe é sugerido uma lista do que é possível importar:

Ctrl+Space para mostrar o que é possível importar do módulo


Exportações nomeadas

Conseguimos redefinir o nome dos objetos e funções que estamos exportando, caso tenhamos uma nomenclatura interna e desejemos que os tais sejam extraídos com nomes mais acessíveis.

Se temos um submódulo com o nome getnextweek, podemos exportá-lo seguindo o padrão camelCasing:

export { getnextweek as getNextWeek } from './getnextweek';
Enter fullscreen mode Exit fullscreen mode

Como exemplo, date-fns, uma biblioteca para tratamento de datas, utiliza exportações nomeadas no barrel base:


A indecisão

É provável que você tenha se perguntado a diferença em utilizar export * from vs export {} from. A reposta que eu daria: depende do contexto. No exemplo do @angular/router as duas formas de exportação são usadas. Preferencialmente, opte por exportações descritivas, ou melhor dizendo, export explícito, assim podemos controlar melhor o que de fato queremos expor.

export * from adiciona um toque de "mágica". Controlar o que estamos exportando é um pouco mais difícil, já que precisaremos ir no arquivo que é re-exportado para ter uma melhor noção do que realmente está sendo exposto. Por outro lado, se você tem uma API muito bem definida, partes importantes do seu módulo que não são constantemente alteradas, é plausível utilizar esta forma "mágica".

Avalie o seu contexto e aplique como melhor planejar. Não é uma regra. Não terceirize suas decisões.


Conclusão

Entendemos uma abordagem para reduzir as repetições em importações, melhorar o compartilhamento de código entre módulos especializados, encapsular caminhos de arquivos e reduzir quebra de dependências, e também uma forma mais elegante de manter nosso código.

Portanto, use este pattern com cautela. Há contextos em que ele é muito útil. O problema não está no Barrel Pattern, e sim no mau uso do mesmo.

Referências


Soli Deo Gloria

Discussion

pic
Editor guide