O guia completo para padrões de design JavaScript

Publicados: 2022-03-11

Como um bom desenvolvedor JavaScript, você se esforça para escrever um código limpo, saudável e sustentável. Você resolve desafios interessantes que, embora únicos, não exigem necessariamente soluções únicas. Você provavelmente já se viu escrevendo um código que se parece com a solução de um problema totalmente diferente que você já tratou antes. Você pode não saber, mas usou um padrão de design JavaScript . Padrões de projeto são soluções reutilizáveis ​​para problemas comuns no projeto de software.

O guia completo para padrões de design JavaScript

Durante a vida útil de qualquer linguagem, muitas dessas soluções reutilizáveis ​​são feitas e testadas por um grande número de desenvolvedores da comunidade dessa linguagem. É por causa dessa experiência combinada de muitos desenvolvedores que essas soluções são tão úteis porque nos ajudam a escrever código de maneira otimizada e, ao mesmo tempo, resolver o problema em questão.

Os principais benefícios que obtemos dos padrões de design são os seguintes:

  • Eles são soluções comprovadas: como os padrões de projeto são frequentemente usados ​​por muitos desenvolvedores, você pode ter certeza de que eles funcionam. E não apenas isso, você pode ter certeza de que eles foram revisados ​​várias vezes e as otimizações provavelmente foram implementadas.
  • Eles são facilmente reutilizáveis: os padrões de design documentam uma solução reutilizável que pode ser modificada para resolver vários problemas específicos, pois não estão vinculados a um problema específico.
  • Eles são expressivos: os padrões de design podem explicar uma grande solução de forma bastante elegante.
  • Eles facilitam a comunicação: quando os desenvolvedores estão familiarizados com os padrões de projeto, eles podem se comunicar mais facilmente sobre possíveis soluções para um determinado problema.
  • Eles evitam a necessidade de refatoração de código: se um aplicativo é escrito com padrões de design em mente, geralmente é o caso de você não precisar refatorar o código mais tarde, porque aplicar o padrão de design correto a um determinado problema já é um ótimo solução.
  • Eles reduzem o tamanho da base de código: como os padrões de design geralmente são soluções elegantes e ideais, eles geralmente exigem menos código do que outras soluções.

Eu sei que você está pronto para entrar neste ponto, mas antes de aprender tudo sobre padrões de design, vamos revisar alguns conceitos básicos de JavaScript.

Uma Breve História do JavaScript

JavaScript é uma das linguagens de programação mais populares para desenvolvimento web hoje. Ele foi feito inicialmente como uma espécie de “cola” para vários elementos HTML exibidos, conhecidos como linguagem de script do lado do cliente, para um dos navegadores iniciais. Chamado Netscape Navigator, ele só podia exibir HTML estático na época. Como você pode supor, a ideia de tal linguagem de script levou a guerras de navegadores entre os grandes players da indústria de desenvolvimento de navegadores da época, como Netscape Communications (hoje Mozilla), Microsoft e outros.

Cada um dos grandes players queria implementar sua própria implementação dessa linguagem de script, então a Netscape criou o JavaScript (na verdade, Brendan Eich fez), a Microsoft fez o JScript e assim por diante. Como você pode imaginar, as diferenças entre essas implementações eram grandes, então o desenvolvimento para navegadores da web foi feito por navegador, com adesivos de melhor visualização que vinham com uma página da web. Logo ficou claro que precisávamos de um padrão, uma solução entre navegadores que unificasse o processo de desenvolvimento e simplificasse a criação de páginas da web. O que eles criaram é chamado ECMAScript.

ECMAScript é uma especificação de linguagem de script padronizada que todos os navegadores modernos tentam suportar, e existem várias implementações (você poderia dizer dialetos) de ECMAScript. O mais popular é o tópico deste artigo, JavaScript. Desde seu lançamento inicial, o ECMAScript padronizou muitas coisas importantes e, para aqueles mais interessados ​​nos detalhes, há uma lista detalhada de itens padronizados para cada versão do ECMAScript disponível na Wikipedia. O suporte do navegador para ECMAScript versões 6 (ES6) e superiores ainda está incompleto e precisa ser transpilado para o ES5 para ser totalmente suportado.

O que é JavaScript?

Para compreender completamente o conteúdo deste artigo, vamos fazer uma introdução a algumas características de linguagem muito importantes que precisamos conhecer antes de mergulhar nos padrões de design JavaScript. Se alguém lhe perguntasse “O que é JavaScript?” você pode responder em algum lugar nas linhas de:

JavaScript é uma linguagem de programação leve, interpretada e orientada a objetos com funções de primeira classe mais comumente conhecidas como linguagem de script para páginas da web.

A definição acima significa dizer que o código JavaScript tem pouca memória, é fácil de implementar e fácil de aprender, com uma sintaxe semelhante a linguagens populares como C++ e Java. É uma linguagem de script, o que significa que seu código é interpretado em vez de compilado. Ele tem suporte para estilos de programação procedural, orientado a objetos e funcional, o que o torna muito flexível para os desenvolvedores.

Até agora, demos uma olhada em todas as características que soam como muitas outras linguagens por aí, então vamos dar uma olhada no que é específico sobre JavaScript em relação a outras linguagens. Vou listar algumas características e dar o meu melhor para explicar por que elas merecem atenção especial.

JavaScript suporta funções de primeira classe

Essa característica costumava ser problemática para mim quando eu estava começando com JavaScript, já que eu vinha de um background em C/C++. JavaScript trata funções como cidadãos de primeira classe, o que significa que você pode passar funções como parâmetros para outras funções como faria com qualquer outra variável.

 // we send in the function as an argument to be // executed from inside the calling function function performOperation(a, b, cb) { var c = a + b; cb(c); } performOperation(2, 3, function(result) { // prints out 5 console.log("The result of the operation is " + result); })

JavaScript é baseado em protótipo

Como é o caso de muitas outras linguagens orientadas a objetos, JavaScript suporta objetos, e um dos primeiros termos que vem à mente quando se pensa em objetos é classes e herança. É aqui que fica um pouco complicado, pois a linguagem não suporta classes em sua forma de linguagem simples, mas usa algo chamado herança baseada em protótipo ou baseada em instância.

É só agora, no ES6, que o termo formal class é introduzido, o que significa que os navegadores ainda não suportam isso (se você se lembrar, até o momento, a última versão ECMAScript totalmente suportada é 5.1). É importante notar, no entanto, que mesmo que o termo “classe” seja introduzido no JavaScript, ele ainda utiliza herança baseada em protótipo sob o capô.

A programação baseada em protótipos é um estilo de programação orientada a objetos em que a reutilização de comportamento (conhecida como herança) é realizada por meio de um processo de reutilização de objetos existentes por meio de delegações que servem como protótipos. Vamos mergulhar em mais detalhes com isso quando chegarmos à seção de padrões de design do artigo, pois essa característica é usada em muitos padrões de design JavaScript.

Loops de eventos JavaScript

Se você tem experiência em trabalhar com JavaScript, certamente está familiarizado com o termo função de retorno de chamada. Para aqueles que não estão familiarizados com o termo, uma função de retorno de chamada é uma função enviada como parâmetro (lembre-se, JavaScript trata funções como cidadãos de primeira classe) para outra função e é executada após o disparo de um evento. Isso geralmente é usado para assinar eventos como um clique do mouse ou um botão do teclado.

Representação gráfica do loop de eventos JavaScript

Cada vez que um evento, que tem um ouvinte anexado a ele, é acionado (caso contrário, o evento é perdido), uma mensagem está sendo enviada para uma fila de mensagens que estão sendo processadas de forma síncrona, de maneira FIFO (first-in-first-out ). Isso é chamado de loop de eventos .

Cada uma das mensagens na fila tem uma função associada a ela. Depois que uma mensagem é removida da fila, o tempo de execução executa a função completamente antes de processar qualquer outra mensagem. Ou seja, se uma função contém outras chamadas de função, todas elas são executadas antes do processamento de uma nova mensagem da fila. Isso é chamado de execução até a conclusão.

 while (queue.waitForMessage()) { queue.processNextMessage(); }

O queue.waitForMessage() espera de forma síncrona por novas mensagens. Cada uma das mensagens processadas tem sua própria pilha e é processada até que a pilha esteja vazia. Uma vez finalizado, uma nova mensagem é processada da fila, se houver.

Você também deve ter ouvido que o JavaScript não é bloqueante, o que significa que quando uma operação assíncrona está sendo executada, o programa é capaz de processar outras coisas, como receber entrada do usuário, enquanto aguarda a conclusão da operação assíncrona, não bloqueando a função principal. fio de execução. Esta é uma propriedade muito útil do JavaScript e um artigo inteiro poderia ser escrito apenas sobre este tópico; no entanto, está fora do escopo deste artigo.

O que são padrões de projeto?

Como eu disse antes, os padrões de projeto são soluções reutilizáveis ​​para problemas comuns no projeto de software. Vamos dar uma olhada em algumas das categorias de padrões de projeto.

Protopadrões

Como se cria um padrão? Digamos que você reconheceu um problema que ocorre com frequência e tem sua própria solução exclusiva para esse problema, que não é reconhecida e documentada globalmente. Você usa essa solução toda vez que encontra esse problema e acha que ela é reutilizável e que a comunidade de desenvolvedores pode se beneficiar dela.

Torna-se imediatamente um padrão? Por sorte, não. Muitas vezes, pode-se ter boas práticas de escrita de código e simplesmente confundir algo que parece um padrão com um quando, na verdade, não é um padrão.

Como você pode saber quando o que você acha que reconhece é na verdade um padrão de projeto?

Obtendo a opinião de outros desenvolvedores sobre isso, conhecendo o processo de criação de um padrão em si e se familiarizando com os padrões existentes. Há uma fase pela qual um padrão deve passar antes de se tornar um padrão completo, e isso é chamado de protopadrão.

Um proto-padrão é um padrão a ser se passar por um certo período de testes por vários desenvolvedores e cenários em que o padrão se mostra útil e fornece resultados corretos. Há uma grande quantidade de trabalho e documentação – a maioria dos quais está fora do escopo deste artigo – a ser feito para que um padrão de pleno direito seja reconhecido pela comunidade.

Antipadrões

Assim como um padrão de projeto representa uma boa prática, um antipadrão representa uma prática ruim.

Um exemplo de antipadrão seria modificar o protótipo da classe Object . Quase todos os objetos em JavaScript herdam de Object (lembre-se de que JavaScript usa herança baseada em protótipo), então imagine um cenário em que você alterou esse protótipo. As alterações no protótipo Object seriam vistas em todos os objetos que herdam desse protótipo — que seriam a maioria dos objetos JavaScript . Isso é um desastre esperando para ocorrer.

Outro exemplo, semelhante ao mencionado acima, é modificar objetos que você não possui. Um exemplo disso seria substituir uma função de um objeto usado em muitos cenários em todo o aplicativo. Se você estiver trabalhando com uma equipe grande, imagine a confusão que isso causaria; você rapidamente se depararia com colisões de nomes, implementações incompatíveis e pesadelos de manutenção.

Assim como é útil conhecer todas as boas práticas e soluções, também é muito importante conhecer as ruins. Dessa forma, você pode reconhecê-los e evitar cometer o erro logo de cara.

Categorização de padrões de design

Os padrões de design podem ser categorizados de várias maneiras, mas a mais popular é a seguinte:

  • Padrões de design de criação
  • Padrões de projeto estrutural
  • Padrões de design comportamental
  • Padrões de design de simultaneidade
  • Padrões de projeto arquitetônico

Padrões de design de criação

Esses padrões lidam com mecanismos de criação de objetos que otimizam a criação de objetos em comparação com uma abordagem básica. A forma básica de criação de objetos pode resultar em problemas de design ou em complexidade adicional ao design. Padrões de design criacional resolvem esse problema controlando de alguma forma a criação de objetos. Alguns dos padrões de design populares nesta categoria são:

  • Método de fábrica
  • Fábrica abstrata
  • Construtor
  • Protótipo
  • Singleton

Padrões de Projeto Estrutural

Esses padrões lidam com relacionamentos de objetos. Eles garantem que, se uma parte de um sistema mudar, o sistema inteiro não precisará mudar junto com ela. Os padrões mais populares nesta categoria são:

  • Adaptador
  • Ponte
  • Composto
  • Decorador
  • Fachada
  • Peso mosca
  • Procuração

Padrões de Design Comportamental

Esses tipos de padrões reconhecem, implementam e melhoram a comunicação entre objetos diferentes em um sistema. Eles ajudam a garantir que partes diferentes de um sistema tenham informações sincronizadas. Exemplos populares desses padrões são:

  • Cadeia de responsabilidade
  • Comando
  • Iterador
  • Mediador
  • Lembrança
  • Observador
  • Estado
  • Estratégia
  • Visitante

Padrões de design de simultaneidade

Esses tipos de padrões de projeto lidam com paradigmas de programação multithread. Alguns dos mais populares são:

  • Objeto ativo
  • Reação nuclear
  • Agendador

Padrões de projeto arquitetônico

Padrões de projeto que são usados ​​para fins arquitetônicos. Alguns dos mais famosos são:

  • MVC (Model-View-Controller)
  • MVP (Model-View-Apresentador)
  • MVVM (Model-View-ViewModel)

Na seção a seguir, vamos dar uma olhada em alguns dos padrões de projeto mencionados acima com exemplos fornecidos para melhor compreensão.

Exemplos de padrões de design

Cada um dos padrões de projeto representa um tipo específico de solução para um tipo específico de problema. Não existe um conjunto universal de padrões que seja sempre o mais adequado. Precisamos aprender quando um padrão específico se mostrará útil e se fornecerá valor real. Uma vez que estamos familiarizados com os padrões e cenários para os quais eles são mais adequados, podemos determinar facilmente se um padrão específico é ou não adequado para um determinado problema.

Lembre-se, aplicar o padrão errado a um determinado problema pode levar a efeitos indesejáveis, como complexidade de código desnecessária, sobrecarga desnecessária no desempenho ou até mesmo a geração de um novo antipadrão.

Todas essas são coisas importantes a serem consideradas ao pensar em aplicar um padrão de design ao nosso código. Vamos dar uma olhada em alguns dos padrões de design que eu pessoalmente achei úteis e acredito que todo desenvolvedor sênior de JavaScript deve estar familiarizado.

Padrão Construtor

Ao pensar em linguagens clássicas orientadas a objetos, um construtor é uma função especial em uma classe que inicializa um objeto com algum conjunto de valores padrão e/ou enviados.

Maneiras comuns de criar objetos em JavaScript são as três seguintes maneiras:

 // either of the following ways can be used to create a new object var instance = {}; // or var instance = Object.create(Object.prototype); // or var instance = new Object();

Após criar um objeto, existem quatro maneiras (desde o ES3) de adicionar propriedades a esses objetos. Eles são os seguintes:

 // supported since ES3 // the dot notation instance.key = "A key's value"; // the square brackets notation instance["key"] = "A key's value"; // supported since ES5 // setting a single property using Object.defineProperty Object.defineProperty(instance, "key", { value: "A key's value", writable: true, enumerable: true, configurable: true }); // setting multiple properties using Object.defineProperties Object.defineProperties(instance, { "firstKey": { value: "First key's value", writable: true }, "secondKey": { value: "Second key's value", writable: false } });

A maneira mais popular de criar objetos são os colchetes e, para adicionar propriedades, a notação de ponto ou colchetes. Qualquer pessoa com alguma experiência com JavaScript já os usou.

Mencionamos anteriormente que JavaScript não suporta classes nativas, mas suporta construtores através do uso de uma palavra-chave “new” prefixada para uma chamada de função. Dessa forma, podemos usar a função como construtor e inicializar suas propriedades da mesma forma que faríamos com um construtor de linguagem clássico.

 // we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; this.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();

No entanto, ainda há espaço para melhorias aqui. Se você se lembrar, mencionei anteriormente que JavaScript usa herança baseada em protótipo. O problema com a abordagem anterior é que o método writesCode é redefinido para cada uma das instâncias do construtor Person . Podemos evitar isso definindo o método no protótipo da função:

 // we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; } // we extend the function's prototype Person.prototype.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();

Agora, ambas as instâncias do construtor Person podem acessar uma instância compartilhada do método writesCode() .

Padrão do módulo

No que diz respeito às peculiaridades, o JavaScript nunca deixa de surpreender. Outra coisa peculiar ao JavaScript (pelo menos no que diz respeito às linguagens orientadas a objetos) é que o JavaScript não suporta modificadores de acesso. Em uma linguagem OOP clássica, um usuário define uma classe e determina os direitos de acesso para seus membros. Como o JavaScript em sua forma simples não suporta classes nem modificadores de acesso, os desenvolvedores de JavaScript descobriram uma maneira de imitar esse comportamento quando necessário.

Antes de entrarmos nos detalhes do padrão do módulo, vamos falar sobre o conceito de encerramento. Um encerramento é uma função com acesso ao escopo pai, mesmo após o fechamento da função pai. Eles nos ajudam a imitar o comportamento dos modificadores de acesso por meio do escopo. Vamos mostrar isso através de um exemplo:

 // we used an immediately invoked function expression // to create a private variable, counter var counterIncrementer = (function() { var counter = 0; return function() { return ++counter; }; })(); // prints out 1 console.log(counterIncrementer()); // prints out 2 console.log(counterIncrementer()); // prints out 3 console.log(counterIncrementer());

Como você pode ver, usando o IIFE, vinculamos a variável counter a uma função que foi invocada e fechada, mas ainda pode ser acessada pela função filho que a incrementa. Como não podemos acessar a variável counter de fora da expressão da função, a tornamos privada por meio da manipulação do escopo.

Usando os closures, podemos criar objetos com partes privadas e públicas. Eles são chamados de módulos e são muito úteis sempre que queremos ocultar certas partes de um objeto e expor apenas uma interface ao usuário do módulo. Vamos mostrar isso em um exemplo:

 // through the use of a closure we expose an object // as a public API which manages the private objects array var collection = (function() { // private members var objects = []; // public members return { addObject: function(object) { objects.push(object); }, removeObject: function(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } }, getObjects: function() { return JSON.parse(JSON.stringify(objects)); } }; })(); collection.addObject("Bob"); collection.addObject("Alice"); collection.addObject("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(collection.getObjects()); collection.removeObject("Alice"); // prints ["Bob", "Franck"] console.log(collection.getObjects());

A coisa mais útil que este padrão introduz é a clara separação das partes privada e pública de um objeto, que é um conceito muito semelhante aos desenvolvedores vindos de um background clássico orientado a objetos.

No entanto, nem tudo é tão perfeito. Quando você deseja alterar a visibilidade de um membro, você precisa modificar o código onde quer que tenha usado esse membro devido à natureza diferente do acesso a partes públicas e privadas. Além disso, os métodos adicionados ao objeto após sua criação não podem acessar os membros privados do objeto.

Revelando o Padrão do Módulo

Esse padrão é uma melhoria feita no padrão do módulo, conforme ilustrado acima. A principal diferença é que escrevemos toda a lógica do objeto no escopo privado do módulo e então simplesmente expomos as partes que queremos que sejam públicas retornando um objeto anônimo. Também podemos alterar a nomenclatura de membros privados ao mapear membros privados para seus membros públicos correspondentes.

 // we write the entire object logic as private members and // expose an anonymous object which maps members we wish to reveal // to their corresponding public members var namesCollection = (function() { // private members var objects = []; function addObject(object) { objects.push(object); } function removeObject(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } } function getObjects() { return JSON.parse(JSON.stringify(objects)); } // public members return { addName: addObject, removeName: removeObject, getNames: getObjects }; })(); namesCollection.addName("Bob"); namesCollection.addName("Alice"); namesCollection.addName("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(namesCollection.getNames()); namesCollection.removeName("Alice"); // prints ["Bob", "Franck"] console.log(namesCollection.getNames());

O padrão de módulo revelador é uma das três maneiras pelas quais podemos implementar um padrão de módulo. As diferenças entre o padrão de módulo revelador e as outras variantes do padrão de módulo estão principalmente em como os membros públicos são referenciados. Como resultado, o padrão do módulo revelador é muito mais fácil de usar e modificar; no entanto, pode ser frágil em certos cenários, como usar objetos RMP como protótipos em uma cadeia de herança. As situações problemáticas são as seguintes:

  1. Se tivermos uma função privada que está se referindo a uma função pública, não podemos substituir a função pública, pois a função privada continuará se referindo à implementação privada da função, introduzindo assim um bug em nosso sistema.
  2. Se tivermos um membro público apontando para uma variável privada e tentarmos substituir o membro público de fora do módulo, as outras funções ainda se referirão ao valor privado da variável, introduzindo um bug em nosso sistema.

Padrão Singleton

O padrão singleton é usado em cenários em que precisamos exatamente de uma instância de uma classe. Por exemplo, precisamos ter um objeto que contenha alguma configuração para alguma coisa. Nesses casos, não é necessário criar um novo objeto sempre que o objeto de configuração for necessário em algum lugar do sistema.

 var singleton = (function() { // private singleton value which gets initialized only once var config; function initializeConfiguration(values){ this.randomNumber = Math.random(); values = values || {}; this.number = values.number || 5; this.size = values.size || 10; } // we export the centralized method for retrieving the singleton value return { getConfig: function(values) { // we initialize the singleton value only once if (config === undefined) { config = new initializeConfiguration(values); } // and return the same config value wherever it is asked for return config; } }; })(); var configObject = singleton.getConfig({ "size": 8 }); // prints number: 5, size: 8, randomNumber: someRandomDecimalValue console.log(configObject); var configObject1 = singleton.getConfig({ "number": 8 }); // prints number: 5, size: 8, randomNumber: same randomDecimalValue as in first config console.log(configObject1);

Como você pode ver no exemplo, o número aleatório gerado é sempre o mesmo, assim como os valores de configuração enviados.

É importante notar que o ponto de acesso para recuperar o valor singleton precisa ser apenas um e muito conhecido. Uma desvantagem de usar esse padrão é que é bastante difícil de testar.

Padrão de observador

O padrão observador é uma ferramenta muito útil quando temos um cenário onde precisamos melhorar a comunicação entre partes distintas do nosso sistema de forma otimizada. Promove o acoplamento frouxo entre os objetos.

Existem várias versões desse padrão, mas em sua forma mais básica, temos duas partes principais do padrão. O primeiro é um sujeito e o segundo são observadores.

Um assunto lida com todas as operações relacionadas a um determinado tópico que os observadores assinam. Essas operações inscrevem um observador em um determinado tópico, cancelam a inscrição de um observador em um determinado tópico e notificam os observadores sobre um determinado tópico quando um evento é publicado.

No entanto, existe uma variação desse padrão chamada padrão de editor/assinante, que usarei como exemplo nesta seção. A principal diferença entre um padrão de observador clássico e o padrão de editor/assinante é que editor/assinante promove um acoplamento ainda mais solto do que o padrão de observador.

No padrão observador, o sujeito mantém as referências aos observadores inscritos e chama métodos diretamente dos próprios objetos enquanto que, no padrão publicador/assinante, temos canais, que servem como ponte de comunicação entre um assinante e um publicador. O editor dispara um evento e simplesmente executa a função de retorno de chamada enviada para esse evento.

Vou mostrar um pequeno exemplo do padrão editor/assinante, mas para os interessados, um exemplo clássico de padrão observador pode ser facilmente encontrado online.

 var publisherSubscriber = {}; // we send in a container object which will handle the subscriptions and publishings (function(container) { // the id represents a unique subscription id to a topic var id = 0; // we subscribe to a specific topic by sending in // a callback function to be executed on event firing container.subscribe = function(topic, f) { if (!(topic in container)) { container[topic] = []; } container[topic].push({ "id": ++id, "callback": f }); return id; } // each subscription has its own unique ID, which we use // to remove a subscriber from a certain topic container.unsubscribe = function(topic, id) { var subscribers = []; for (var subscriber of container[topic]) { if (subscriber.id !== id) { subscribers.push(subscriber); } } container[topic] = subscribers; } container.publish = function(topic, data) { for (var subscriber of container[topic]) { // when executing a callback, it is usually helpful to read // the documentation to know which arguments will be // passed to our callbacks by the object firing the event subscriber.callback(data); } } })(publisherSubscriber); var subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Bob's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID2 = publisherSubscriber.subscribe("mouseHovered", function(data) { console.log("I am Bob's callback function for a hovered mouse event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID3 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Alice's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); // NOTE: after publishing an event with its data, all of the // subscribed callbacks will execute and will receive // a data object from the object firing the event // there are 3 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"}); // we unsubscribe from an event by removing the subscription ID publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3); // there are 2 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"});

Esse padrão de design é útil em situações em que precisamos executar várias operações em um único evento sendo acionado. Imagine que você tenha um cenário em que precisamos fazer várias chamadas AJAX para um serviço de back-end e, em seguida, realizar outras chamadas AJAX, dependendo do resultado. Você teria que aninhar as chamadas AJAX uma dentro da outra, possivelmente entrando em uma situação conhecida como callback hell. Usar o padrão de editor/assinante é uma solução muito mais elegante.

Uma desvantagem de usar esse padrão é o teste difícil de várias partes do nosso sistema. Não há uma maneira elegante de sabermos se as partes assinantes do sistema estão se comportando conforme o esperado.

Padrão Mediador

Abordaremos brevemente um padrão que também é muito útil quando falamos de sistemas desacoplados. Quando temos um cenário em que várias partes de um sistema precisam se comunicar e ser coordenadas, talvez uma boa solução seja introduzir um mediador.

Um mediador é um objeto que é usado como ponto central para comunicação entre partes díspares de um sistema e lida com o fluxo de trabalho entre elas. Agora, é importante ressaltar que ele lida com o fluxo de trabalho. Por que isso é importante?

Porque há uma grande semelhança com o padrão de editor/assinante. Você pode se perguntar, OK, então esses dois padrões ajudam a implementar uma melhor comunicação entre os objetos... Qual é a diferença?

A diferença é que um mediador lida com o fluxo de trabalho, enquanto o editor/assinante usa algo chamado de tipo de comunicação “dispare e esqueça”. O editor/assinante é simplesmente um agregador de eventos, o que significa que ele simplesmente se encarrega de disparar os eventos e informar aos assinantes corretos quais eventos foram disparados. O agregador de eventos não se importa com o que acontece depois que um evento é disparado, o que não é o caso de um mediador.

Um bom exemplo de mediador é um tipo de interface de assistente. Digamos que você tenha um grande processo de registro para um sistema em que trabalhou. Muitas vezes, quando muitas informações são exigidas de um usuário, é uma boa prática dividir isso em várias etapas.

Dessa forma, o código ficará muito mais limpo (mais fácil de manter) e o usuário não ficará sobrecarregado com a quantidade de informações que são solicitadas apenas para finalizar o cadastro. Um mediador é um objeto que lidaria com as etapas de registro, levando em consideração diferentes fluxos de trabalho possíveis que podem ocorrer devido ao fato de que cada usuário pode ter um processo de registro único.

O benefício óbvio desse padrão de design é a comunicação aprimorada entre as diferentes partes de um sistema, que agora se comunicam por meio do mediador e da base de código mais limpa.

Uma desvantagem seria que agora introduzimos um único ponto de falha em nosso sistema, ou seja, se nosso mediador falhar, todo o sistema poderá parar de funcionar.

Padrão de protótipo

Como já mencionamos ao longo do artigo, JavaScript não suporta classes em sua forma nativa. A herança entre objetos é implementada usando programação baseada em protótipos.

Ele nos permite criar objetos que podem servir de protótipo para outros objetos que estão sendo criados. O objeto protótipo é usado como um modelo para cada objeto que o construtor cria.

Como já falamos sobre isso nas seções anteriores, vamos mostrar um exemplo simples de como esse padrão pode ser usado.

 var personPrototype = { sayHi: function() { console.log("Hello, my name is " + this.name + ", and I am " + this.age); }, sayBye: function() { console.log("Bye Bye!"); } }; function Person(name, age) { name = name || "John Doe"; age = age || 26; function constructorFunction(name, age) { this.name = name; this.age = age; }; constructorFunction.prototype = personPrototype; var instance = new constructorFunction(name, age); return instance; } var person1 = Person(); var person2 = Person("Bob", 38); // prints out Hello, my name is John Doe, and I am 26 person1.sayHi(); // prints out Hello, my name is Bob, and I am 38 person2.sayHi();

Take notice how prototype inheritance makes a performance boost as well because both objects contain a reference to the functions which are implemented in the prototype itself, instead of in each of the objects.

Command Pattern

The command pattern is useful in cases when we want to decouple objects executing the commands from objects issuing the commands. For example, imagine a scenario where our application is using a large number of API service calls. Then, let's say that the API services change. We would have to modify the code wherever the APIs that changed are called.

This would be a great place to implement an abstraction layer, which would separate the objects calling an API service from the objects which are telling them when to call the API service. This way, we avoid modification in all of the places where we have a need to call the service, but rather have to change only the objects which are making the call itself, which is only one place.

As with any other pattern, we have to know when exactly is there a real need for such a pattern. We need to be aware of the tradeoff we are making, as we are adding an additional abstraction layer over the API calls, which will reduce performance but potentially save a lot of time when we need to modify objects executing the commands.

 // the object which knows how to execute the command var invoker = { add: function(x, y) { return x + y; }, subtract: function(x, y) { return x - y; } } // the object which is used as an abstraction layer when // executing commands; it represents an interface // toward the invoker object var manager = { execute: function(name, args) { if (name in invoker) { return invoker[name].apply(invoker, [].slice.call(arguments, 1)); } return false; } } // prints 8 console.log(manager.execute("add", 3, 5)); // prints 2 console.log(manager.execute("subtract", 5, 3));

Facade Pattern

The facade pattern is used when we want to create an abstraction layer between what is shown publicly and what is implemented behind the curtain. It is used when an easier or simpler interface to an underlying object is desired.

A great example of this pattern would be selectors from DOM manipulation libraries such as jQuery, Dojo, or D3. You might have noticed using these libraries that they have very powerful selector features; you can write in complex queries such as:

 jQuery(".parent .child div.span")

It simplifies the selection features a lot, and even though it seems simple on the surface, there is an entire complex logic implemented under the hood in order for this to work.

We also need to be aware of the performance-simplicity tradeoff. It is desirable to avoid extra complexity if it isn't beneficial enough. In the case of the aforementioned libraries, the tradeoff was worth it, as they are all very successful libraries.

Próximos passos

Design patterns are a very useful tool which any senior JavaScript developer should be aware of. Knowing the specifics regarding design patterns could prove incredibly useful and save you a lot of time in any project's lifecycle, especially the maintenance part. Modifying and maintaining systems written with the help of design patterns which are a good fit for the system's needs could prove invaluable.

In order to keep the article relatively brief, we will not be displaying any more examples. For those interested, a great inspiration for this article came from the Gang of Four book Design Patterns: Elements of Reusable Object-Oriented Software and Addy Osmani's Learning JavaScript Design Patterns . I highly recommend both books.

Relacionado: Como desenvolvedor JS, isso é o que me mantém acordado à noite / Entendendo a confusão de classe ES6