Namespaces: como evitar conflitos no código em JavaScript

Namespaces: como evitar conflitos no código em JavaScript
Camila Pessôa
Camila Pessôa

Compartilhe

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!

O gif apresenta uma animação com três bebês fisicamente iguais, cada um com uma fantasia de um animal. O primeiro da esquerda para a direita está vestido de urso, o do meio está vestido de sapo e o último, à direita, está vestido de gato. Os três fazem uma expressão de confusão como se alguém estivesse chamando todos ao mesmo tempo.

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.

Banner da promoção da black friday, com os dizeres: A Black Friday Alura está chegando. Faça parte da Lista VIP, receba o maior desconto do ano em primeira mão e garanta bônus exclusivos. Quero ser VIP

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 é?

A imagem apresenta um meme do desenho Homem-Aranha. No desenho há três pessoas com o uniforme do homem-aranha, dois estão nas extremidades esquerda e direita da tela e o terceiro está mais ao fundo, no centro. O terceiro aponta os dedos indicadores para os que estão no primeiro plano e os dois apontam um para o outro.

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.

A imagem apresenta um meme da animação Toy Story com o personagem Woody, que está com uma vestimenta de cowboy e uma expressão de insatisfação, e Buzz Lightyear, com uma roupa de astronauta. O Buzz está com o braço esquerdo levantado e sua mão aponta para o horizonte, como se apresentasse algo para o personagem Woody. No topo da tela está escrito “namespaces”  e na parte inferior o texto “em todos os lugares”

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!

O gif animado apresenta um um papagaio verde e amarelo, com desenhos de dois braços em formato de palitos em seu corpo, caminhando sobre uma mesa branca e depois dando pequenos saltos levantando os braços desenhados, sugerindo uma comemoração. Na parte inferior da tela há um texto com as letras “JS”, que são um apelido para 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.

O gif apresenta um homem retirando os óculos de sol do rosto, fazendo uma expressão de surpresa. Após retirar o óculos, outro óculos está em seu rosto.

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:

  1. O código define um único objeto global, meuApp, que inicialmente é um objeto vazio.
  2. Uma função anônima é definida e invocada imediatamente usando o método apply. O método apply (nativo do JavaScript) permite que a função seja invocada com um valor this específico, neste caso, o objeto meuApp.
  3. Dentro da função anônima, uma variável id é definida com um valor de 0.
  4. Duas funções são definidas e adicionadas ao objeto meuApp: proxima e redefinir. A função proxima retorna o valor de id e o incrementa em 1 cada vez que é chamado. A função redefinir redefine o valor de id para 0.
  5. A instrução console.log no final do código chama as funções proxima e redefinir no objeto meuApp 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?

Camila Pessôa
Camila Pessôa

Oi oi, sou a Camila ! Ingressei na área de tecnologia por meio da robótica educacional e comecei os estudos em programação com desenvolvimento web e foco Back-end com Node.js. Adoro ler, assistir séries/filmes, animes, jogar e passear ao ar livre com minha filhota.Tenho tenho grande paixão por educação e tecnologia, pois acredito que essa combinação é transformadora! :)

Veja outros artigos sobre Programação