Namespaces: como evitar conflitos no código em JavaScript
Introdução
Imagine que você tem 3 filhas e resolveu dar os nomes: Ana Clara, Ana Cristina e Ana Paula para cada uma delas. Em dado momento chama:
-"Ana, venha até a sala!".
Mas qual Ana você realmente quer chamar? Certamente haverá um conflito aqui!
Fonte: Tumblr
A melhor forma de evitar esse conflito seria chamar especificando qual “Ana”:
-"Ana Clara, venha até a sala!"
Assim todas as irmãs, e quem estivesse presente, saberiam exatamente a quem você está se referindo.
Problemas como esse também podem acontecer em programação, quando utilizamos identificadores iguais em variáveis, funções e classes que apresentam funcionalidades parecidas. Mas para resolver esse e outros tipos de situações existe um conceito muito popular chamado namespaces.
Namespace, literalmente significa Espaço nominal. Mas o que isso quer dizer na prática?
Antes de tudo, precisamos entender que namespace é um conceito, ou seja, não existe uma fórmula única ou mágica para sua aplicação em JavaScript (JS). Um exemplo clássico em JS é criar uma variável com um identificador generalista, como ˋlet name;ˋ em diferentes partes do código para descrever, por exemplo, que recebe o nome de uma pessoa em um formulário. Porém, quando trabalhamos com bibliotecas externas não é possível prever se alguma delas também vai utilizar alguma variável name
em seu código interno e para evitar esses conflitos existe o conceito de namespaces.
Então, ao longo deste artigo, descubra como namespaces funcionam e como aplicá-lo na prática em seus códigos JavaScript!
Para aproveitar melhor esta leitura, sugiro que você tenha uma base da linguagem JavaScript, pois ao final você terá ferramentas para otimizar a escrita de código com namespaces.
O que são os namespaces?
Em computação, namespace define a prática para a criação de um espaço declarativo que fornece um escopo para funções, variáveis, classes, objetos etc. Ainda não parece muito claro?
Um namespace funciona como um contêiner de identificadores, basicamente uma forma de organizar seu código em grupos que façam sentido para qualquer pessoa desenvolvedora que o leia.
Por se tratar de um conceito, na prática o namespacing pode ser aplicado também em outros contextos, tais como em sistemas operacionais Linux, sistemas de orquestração de contêineres como Kubernetes, etc. Porém, neste artigo vamos aprofundar a sua aplicação em linguagens de programação, e mais especificamente em JavaScript.
O namespace é então uma solução e um dos grandes problemas que visa resolver é: evitar as temidas collision names (em tradução literal, colisões de nomes). Ou seja, os conflitos de elementos (variáveis, funções, classes, etc) que apresentam identificadores iguais em seu código.
Você sabia que os conflitos entre nomes iguais são comuns ao utilizar bibliotecas externas ou quando o projeto fica muito extenso?
Isso pode acontecer quando declaramos uma variável, classe ou função que carrega o mesmo identificador de uma biblioteca externa ou de nossa própria base de código. Afinal, projetos crescem de tal maneira que às vezes a criatividade para identificadores pode ir acabando, não é?
Fonte: Pinterest
Dessa maneira, se você possui uma aplicação escalável, é possível que em algum momento seja necessário aplicar o conceito de namespacing. Sobretudo se você está desenvolvendo o projeto com mais de uma pessoa ou vai dar continuidade a um projeto em desenvolvimento.
Mas, além de evitar as colisões de nomes iguais, o namespacing é importante também para manter o seu código limpo e fácil de ser compreendido, pois evita a criação de múltiplas variáveis, funções ou classes com nomes diferentes e também a preocupação em selecionar identificadores únicos.
Pessoas que desenvolvem em C#, C++, PHP ou GOlang provavelmente já fazem uso do namespacing em seu código, principalmente porque essas e outras linguagens fornecem ele como um recurso de forma nativa a partir de uma palavra-chave, como a palavra reservada namespace
em PHP, C# e C++.
Ao analisarmos o uso de namespaces nas linguagens mencionadas, notamos sua aplicação no contexto de orientação a objetos para a nomenclatura de classes, ou seja, você pode nomear classes com o mesmo identificador. Confira mais sobre como funciona a criação de namespaces em PHP no artigo “Organizando seu código com namespace” e C# na apostila “C# - orientação a objetos”, pois ilustram casos típicos de uso dos namespaces.
Mas vale lembrar que o uso de namespaces não está restrito a programação orientada a objetos ou a nomenclatura de classes e pode ser aplicado em funções, variáveis, objetos etc. O importante é manter a ideia principal: permitir o uso de um mesmo identificador através da criação de escopos ou contextos.
Fonte: Meme generator
O namespace funciona como grupo de instruções contidas em um conjunto e para acessá-las devemos utilizar primeiro o nome do grupo e depois a instrução desejada. É uma forma de organização, ou seja, você agrupa instruções e as relaciona a um identificador.
Bom, até aqui aprendemos de forma conceitual o que são os namespaces e como são utilizados no código, e você conferiu a indicação de alguns artigos que apresentam os usos de namespaces em linguagens fortemente tipadas como C# e C++. Porém, algumas perguntas ainda precisam de resposta, principalmente a dúvida : ”como eu consigo criar um namespace na prática?”
E para responder esse tópico, nós vamos utilizar a linguagem JavaScript!
Fonte: Tenor
Além de ser uma das linguagens mais populares da web, escolhemos JavaScript porque não possui uma palavra-chave específica para criação de namespaces como ocorre em outras linguagens. Dessa maneira é possível incorporar e aplicar o conceito de conteinerização de identificadores idênticos com namespacing na prática. Conheça algumas técnicas a seguir.
Namespaces em JavaScript
Já compreendemos que o namespace envolve uma instrução, como uma variável ou método, e mantém ela isolada do escopo global do código. O recurso criado como um namespace é invisível ao escopo global e, em contrapartida, mantém-se acessível apenas para os processos que estão relacionados ao seu identificador, ou seja, ao seu nome.
Para o JavaScript o namespace é muito interessante, sobretudo por conta dos tópicos que envolvem o escopo global e escopo de bloco com variáveis do tipo var
e reatribuição de valores com let
, pois variáveis com os mesmos nomes podem ter seus valores sobrescritos se forem do tipo var e, dependendo do escopo, podem acusar um erro em seu código.
O livro “Eloquent JavaScript” apresenta um dos usos frequentes de namespacing que está embutido na linguagem. Podemos identificá-lo durante a construção de objetos que possuem recursos nativos, como o objeto Math
. Falando de outra maneira, é como se o objeto Math
representasse um conjunto maior, que contém todos os métodos relativos a ele.
O Math
funciona como uma caixa de ferramentas para dados do tipo number
, como o Math.max
(máximo) ou Math.min
(mínimo). O container que agrupa essas funcionalidades do objeto Math
fornece um namespace para que valores e funções não sejam declaradas como variáveis globais no código. Mas o que isso significa? Vamos entender melhor com o exemplo abaixo:
Imagine que você precisa arredondar números fracionários para um valor inteiro. O Math
possui um método para esse fim, o round()
, e você pode testá-lo como no código a seguir:
const round = Math.round(20.8);
console.log(round); //21
Percebeu que não houve conflitos entre o identificador round
e o método round()
? Isso quer dizer que posso utilizar o identificador round
várias vezes?
Vamos testar de outra forma? Analise o próximo exemplo:
const round = 20.8; // variável com identificador round
function round() { //função com o identificador round
return Math.round(round);
}
console.log(round);
console.log(round());
Quando rodamos o código, o console devolve o erro Uncaught SyntaxError: Identifier 'round' has already been declared
. O erro literalmente informa que há um erro de sintaxe e que o identificador round
já foi declarado. O conflito ocorreu porque declaramos uma variável e uma função com o mesmo identificador no escopo global do código. Mas por qual motivo isso acontece, se no primeiro caso conseguimos trabalhar com o método
e criar uma const
?
Na prática sabemos, de maneira intuitiva, que é possível declarar uma nova variável no código com o identificador round
, max
ou min
, pois não vai gerar conflitos. E isso acontece porque os métodos round
, floor
, max
e etc estão contidos dentro do objeto Math
, ou seja, não precisamos nos preocupar em sobrescrever nossos valores! Já tinha parado pra pensar nisso?
E embora o JavaScript não apresente o namespace como funcionalidade por padrão, é possível simular namespaces estáticos e dinâmicos seguindo essa linha de raciocínio.
Vamos conferir algumas das formas mais conhecidas de tratar namespacing em JS, que é a partir de um prefixo em seu identificador.
Namespaces com prefixos ou prefix namespacing
A prática de criação de namespaces com prefixos é bem mais simples do que utilizar objetos ou funções. Além disso, trata-se de um namespace estático, pois sempre fará referência aos mesmos objetos.
Com o código abaixo, criamos um namespace lógico através do uso do mesmo prefixo para variáveis ou funções do mesmo namespace.
//adicione propriedades globais com um prefixo exclusivo
const meuApp_digaOla = function () {
console.log("Olá, Mundo!");
};
const meuApp_digaAdeus = function () {
console.log("Adeus!");
};
// chame a função para usar as propriedades do namespace
meuApp_digaOla();
Saída: "Olá, Mundo!"
Aqui a aplicação é relativamente mais simples pois o prefixo meuApp
é o responsável pela criação do namespace. Uma desvantagem é que deixa o código mais verboso, porém resolve o problema de possíveis colisões de nomes.
Além dessa há outras formas de estabelecer namespace estático, uma delas é via atribuição direta com objetos literais e, embora seja um pouco mais complexa, deixa o código mais organizado.
Notação de objeto literal
Outra forma implementar o namespace é criar um objeto literal para “guardar” diretamente funções e variáveis que pertencem ao namespace, confira o código:
// namespace
const estudante = {
id: "1",
nome: "Carlos",
estadoMatricula: () => {
console.log("Aluno Matriculado");
},
// função
get_Nota: () => {
return "A";
}
};
// imprimir detalhes do namespace
console.log("Nome:", estudante.nome);// "Nome:" "Carlos"
console.log("ID:", estudante.id); //"ID:" "1"
console.log("Nota:", estudante.get_Nota());//"Nota:" "A"
Como é possível perceber, o nome do objeto pode ser usado para acessar todos os membros que estão no namespace. No exemplo, o objeto estudante
é uma única variável global e funciona como um namespace para todas as suas propriedades.
Para acessarmos as propriedades usamos a notação de ponto, ou seja, para acessar uma variável usamos estudante.nome
e para executar uma função usamos estudante.get_Nota()
.
É possível também implementar a partir da criação de um objeto vazio e depois adicionar os membros do namespace via atribuição direta. Vamos conferir no código a seguir:
// objeto com namespace
const estudante = {};
//adicionar membros
estudante.id = "1";
estudante.nome = "Carlos";
//adicionar funções
estudante.estadoMatricula = () => {
console.log(“Aluno Matriculado");
};
estudante.get_Nota = () => {
return "A";
};
No código os elementos são inseridos no objeto estudante
de forma gradual. A grande vantagem dessa prática é que minimiza a poluição do código-fonte através dos objetos globais e sua leitura se torna mais fácil.
Um ponto de atenção é que se chamarmos a função estado()
de forma isolada, sem o namespace estudante
, ela não irá funcionar pois não está acessível ao restante do código, mas apenas naquele namespace. Além disso, é possível utilizar o identificador estado
em outras partes do código.
Como podemos identificar, o que acontece é a criação de um objeto literal com suas propriedades, algo comum do JavaScript. Esse recurso é capaz de simular um namespace no sentido de isolar os nomes das propriedades. A seguir vamos conferir outro padrão bastante comum para o JS, o padrão de módulo.
Namespaces com padrão de módulo ou module pattern
Em primeiro lugar, o padrão de módulo não deve ser confundido com os módulos em JavaScript. Tudo bem, mas por qual motivo você deve conhecer mais um padrão se o Objeto Literal parece funcionar bem?
A notação com objetos literais é um pouco mais fechada e podemos atribuir diretamente as propriedades ao objeto. Também não é possível realizar referências cruzadas com muita facilidade. É nesse contexto que o padrão de módulo se encaixa, pois não possui essas limitações e adiciona a característica de deixar métodos e propriedades em modo privado. Confira no código abaixo:
const pessoa = (function () {
return {
getNome: function () {
const nome = "Camis";
return nome;
},
getIdade: function () {
const idade = 34;
return idade;
}
};
})();
console.log(pessoa.getNome()); //”Camis”
console.log(pessoa.getIdade()); // 34
O padrão de módulo se caracteriza por ser auto executável, é o que chamamos de “Immediately Invoked Function Expressions” (em tradução livre, expressões de funções invocadas imediatamente), e são funções executadas imediatamente após sua declaração. Essa característica das funções IIFE é definida pelos parênteses ()
ao final de sua construção.
Outra característica relevante é que embora o JavaScript não possua métodos privados de forma ”built-in” (em tradução livre, embutido), o padrão de módulo também permite a criação de métodos que funcionam de forma semelhante. É uma forma de evitar vazamento de escopo e conflitos de nomes.
Então, a utilização dos padrões de módulo se torna mais flexível para grandes projetos, levando em consideração a abordagem utilizada e garantindo maior segurança das informações.
Por outro lado, temos uma prática controversa entre as pessoas desenvolvedoras: implementar namespaces aninhados. Vamos entender mais sobre isso no próximo tópico.
Namespaces aninhados ou nested namespaces
A criação de namespaces aninhados é outra prática comum para estender o objeto que abrange o namespace. Basicamente é um namespace dentro do outro, confira no código:
// namespace
const simples_ns = simples_ns || {};
// criando um namespace aninhado
simples_ns.aninhado_ns = (function () {
// objeto dentro do namespace aninhado
const mais_aninhado = {};
mais_aninhado.texto = "Esse é um Namespace aninhado";
// definindo a função
mais_aninhado.iniciar = function () {
console.log("Iniciando um Namespace mais aninhado");
};
// retorno do objeto
return mais_aninhado;
})();
// Chamada do método pelo namespace aninhado
simples_ns.aninhado_ns.iniciar();
console.log("Nome: ", simples_ns.aninhado_ns.texto);
No código apresentado, identificamos o namespace simples_ns
que pode receber seu próprio valor ou um objeto vazio. Em seguida, há o namespace simples_ns.aninhado_ns
que é uma função que armazena outro namespace em seu interior, chamado de mais_aninhado
; por fim, há o retorno do seu valor.
Algumas pessoas desenvolvedoras entendem os namespaces aninhados como uma má prática, e questionam seu uso justificando ser uma prática complexa para evitar colisões de identificadores. A justificativa é que a utilização dos namespaces aninhados parece mais uma herança da linguagem Java, com suas convenções de nomenclatura de packages, do que uma solução realmente pensada de acordo com a estrutura do JavaScript. Nesse sentido, vale sempre refletir se determinada prática faz sentido no seu projeto, pois é comum uma solução simples ser o suficiente em alguns contextos.
Fonte: Tenor
Por outro lado, como já identificamos na notação de objeto literal, há maneiras de utilizarmos a estrutura do JavaScript para criação de namespaces e uma muito prática é com o this
, confira no tópico a seguir.
Namespaces e o this
No código a seguir, identificamos um namespace dinâmico com a palavra-chave this
e o método apply()
. Além disso, o que qualifica o namespace como dinâmico é o fato de ser referenciado dentro de um wrapper (invólucro, em tradução livre) da função, eliminando a necessidade do return
.
O recurso this
pode não parecer muito claro em um primeiro momento mas é bastante útil para aplicação de namespaces, pois é um recurso da própria linguagem e será utilizado da forma que foi projetado (ou seja, sem gambiarra). Vamos conferir o código:
const meuApp = {};
(function () {
const id = 0;
this.proxima = function () {
return id++;
};
this.redefinir = function () {
id = 0;
};
}.apply(meuApp));
console.log(
meuApp.proxima(),
meuApp.proxima(),
meuApp.redefinir(),
meuApp.proxima()
);
A saída será: // 0, 1, undefined, 0
No geral, o código define um contador simples que pode ser incrementado e zerado usando as funções proxima
e redefinir
no objeto meuApp
. Mas vamos analisar passo a passo:
- O código define um único objeto global,
meuApp
, que inicialmente é um objeto vazio. - Uma função anônima é definida e invocada imediatamente usando o método
apply
. O métodoapply
(nativo do JavaScript) permite que a função seja invocada com um valorthis
específico, neste caso, o objetomeuApp
. - Dentro da função anônima, uma variável id é definida com um valor de 0.
- Duas funções são definidas e adicionadas ao objeto
meuApp
:proxima
eredefinir
. A funçãoproxima
retorna o valor deid
e o incrementa em 1 cada vez que é chamado. A funçãoredefinir
redefine o valor deid
para 0. - A instrução
console.log
no final do código chama as funçõesproxima
eredefinir
no objetomeuApp
e registra os valores retornados.
Para aprofundarmos a compreensão do código, o método apply()
funciona para chamar uma função que apresenta um valor this
e argumentos como um array
. A palavra-chave this
é aplicada para fazer referência para o objeto ao qual foi chamada no contexto de execução. E, na conjuntura do código acima, junto com o método apply()
conseguimos criar um namespace. Muito prático, não é?
Namespaces vs Módulos
Aprendemos sobre diversas formas de criar namespaces para organização de código. No entanto, durante a leitura deste texto, em algum momento você pode ter pensado: “Mas e os módulos? Não podem funcionar como namespaces?”
Esse recurso bem característico do JavaScript parece ter muita semelhança com o conceito de namespaces mas não são a mesma coisa.
Um Módulo é uma forma de organizar o código em arquivos separados e você pode executá-los num escopo local. Para utilizar os módulos o JavaScript trabalha com as palavras chave import
e export
e o Node.JS trabalha também com a função require()
e o objeto global module.exports
.
Você pode conferir com mais detalhes o que são os módulos do JavaScript, por que existem as duas formas e como/quando utilizá-las neste artigo.
Segundo o livro “You Don’t Know JavaScript”, um módulo é uma coleção de arquivos e funções relacionadas, caracterizadas pela divisão entre detalhes ocultos, privados ou acessíveis de forma pública, que são normalmente chamadas de “API públicas”. Além disso, o módulo é stateful, isso significa que mantém algumas informações e possui a funcionalidade de acessá-las e atualizá-las.
Em contrapartida, os namespaces funcionam como um agrupamento stateless, (sem estado, que são recursos isolados), pois se trata de um conjunto de funções sem dados. Os namespaces não fazem uso do encapsulamento da mesma forma que os módulos.
Também é possível usar os módulos ESM (ou seja, import
/export
) para criar o chamado namespace import. Para exemplificar, vamos criar uma calculadora com soma e subtração em um arquivo calculadora.js
e exportar duas funções com a palavra-chave export
, acompanhe o código:
// calculadora.js
export function soma(a, b) {
return a + b;
}
export function subtracao(a, b) {
return a - b;
}
Após isso, criamos outro arquivo chamado main.js
e importamos as funções com a palavra-chave import
:
// main.js
import * as calculadora from "./calculadora.js";
console.log(calculadora.soma(1, 2)); // 3
console.log(calculadora.subtracao(1, 2)); // -1
Neste exemplo, temos um arquivo chamado calculadora.js
que exporta duas funções: soma
e subtracao
. No arquivo main.js
, usamos a instrução import
para importar todas as exportações de calculadora.js
e atribuí-las a um objeto chamado calculadora
. Assim, podemos então usar o objeto calculadora
para acessar as funções importadas, como calculadora.soma
e calculadora.subtracao
.
Note que na instrução de importação, o import
deve estar na parte superior do arquivo e na instrução de exportação, o export
deve estar no arquivo do qual você está exportando.
Você pode conferir mais exemplos de módulos em JavaScript neste artigo já citado mais acima.
Conclusão
Ao longo de nossa leitura percebemos a utilidade de namespaces para evitar colisões de nomes e organizar seu código a partir do agrupamento de membros relacionados. Além de evitar possíveis erros ou a sobrescrita de informações, seja quando você trabalha com uma equipe grande ou quando a base de código possui muitas bibliotecas.
Entendemos que os namespaces permitem que você use um mesmo identificador para mais de uma classe, variável ou função. Algo que facilita a prática de desenvolvimento, especialmente quando há códigos muito extensos.
Também entendemos que embora algumas linguagens tenham o recurso do namespace de forma nativa, o JavaScript não possui esse suporte por padrão. No entanto, é possível contornar essas limitações com os recursos presentes na própria linguagem, com o uso de objetos literais, IIFES e módulos.
Você agora é capaz de aplicar na prática o namespace com JavaScript e até mesmo reconhecer quando faz uso do conceito em seu código. Além disso, percebeu que não há uma única forma de criação de namespaces e que isso pode variar de acordo com o seu projeto, pois é a partir da abordagem utilizada que você garantirá um uso eficiente dos recursos do namespace.
Obrigada por chegar até aqui! Continue sua jornada de aprendizagem sobre namespaces em JavaScript acessando as referências, livros e artigos mencionados ao longo da leitura.
Bons estudos e até mais!
Envie sua sugestão de curso pra gente: Formulário para sugestão de Curso
Referências
E então, vamos aprender mais?
- Curso de HTTP - Entendendo a web por baixo dos panos;
- Artigo guia para importação e exportação de modulos em JavaScript;
- Namespaces em C#;
- Namespaces;
- Declarar namespaces para organizar tipos;
- Namespacing in JavaScript;
- JavaScript namespace;
- Namespace C++;
- Namespaces Php;
- Learn namespaces C++;
- Tipos de Namespace em Javascript;
- namespaces em PHP;
- Utilizando Namespacing em JavaScript;
- “You Don’t Know JavaScript”;
- “Eloquent JavaScript”.