Como um bom desenvolvedor de 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 exclusivas. Você provavelmente já escreveu um código que se parece com a solução de um problema totalmente diferente que você já tratou. Você pode não saber, mas usou um JavaScript padrão de design . Os padrões de design são soluções reutilizáveis para problemas comuns no design de software.
Durante a vida de qualquer idioma, muitas dessas soluções reutilizáveis são feitas e testadas por um grande número de desenvolvedores da comunidade desse idioma. É 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:
Sei que você está pronto para começar agora, mas antes de aprender tudo sobre os padrões de design, vamos revisar alguns fundamentos 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 da web iniciais. Chamado Netscape Navigator, ele só podia exibir HTML estático no momento. Como você pode supor, a ideia de tal linguagem de script levou a guerras de navegadores entre os grandes jogadores da indústria de desenvolvimento de navegadores da época, como Netscape Communications (hoje Mozilla), Microsoft e outros.
Cada um dos grandes jogadores queria levar adiante sua própria implementação dessa linguagem de script, então a Netscape fez 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 era feito por navegador, com adesivos mais visualizados que vinham com uma página da web. Logo ficou claro que precisávamos de um padrão, uma solução para vários 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 oferecer suporte, e há 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 superior ainda está incompleto e deve ser transpilado para o ES5 para ser totalmente compatível.
A fim de compreender totalmente o conteúdo deste artigo, vamos fazer uma introdução a algumas características muito importantes da linguagem que precisamos estar cientes 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 mencionada significa que o código JavaScript tem uma pegada de memória baixa, é 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.
você se inscreveu em um serviço baseado em nuvem para sincronizar dados entre
Essa característica costumava ser difícil de entender quando eu estava apenas começando com JavaScript, já que eu tinha experiência em C / C ++. JavaScript trata as funções como cidadãos de primeira classe, o que significa que você pode passar funções como parâmetros para outras funções da mesma forma que 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); })
Como é o caso de muitas outras linguagens orientadas a objetos, JavaScript oferece suporte a objetos, e um dos primeiros termos que vêm à mente quando se pensa em objetos é classes e herança. É aqui que fica um pouco complicado, já que a linguagem não oferece suporte a classes em sua forma de linguagem simples, mas sim usa algo chamado herança baseada em protótipo ou baseada em instância.
Só agora, no ES6, o termo formal classe é introduzido, o que significa que os navegadores ainda não oferecem suporte a isso (se você se lembra, no momento da escrita, a última versão ECMAScript totalmente compatível é 5.1). É importante observar, entretanto, que embora 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ótipo é 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. Iremos nos aprofundar em mais detalhes quando chegarmos à seção de padrões de design do artigo, já que essa característica é usada em muitos padrões de design JavaScript.
Se você tem experiência em trabalhar com JavaScript, certamente está familiarizado com o termo função de retorno de chamada . Para quem não está familiarizado com o termo, uma função de retorno de chamada é uma função enviada como um parâmetro (lembre-se de que o JavaScript trata as funções como cidadãos de primeira classe) para outra função e é executada depois que um evento é disparado. Isso geralmente é usado para inscrever-se em eventos como um clique do mouse ou um pressionamento de botão do teclado.
Cada vez que um evento, que tem um ouvinte anexado a ele, dispara (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 forma FIFO (first-in-first-out ) Isso é chamado de loop de eventos .
Cada uma das mensagens na fila possui uma função associada a ela. Depois que uma mensagem é retirada 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 realizadas 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()
aguarda sincronizadamente por novas mensagens. Cada uma das mensagens sendo processadas tem sua própria pilha e é processada até que a pilha esteja vazia. Assim que terminar, uma nova mensagem é processada da fila, se houver.
Você também deve ter ouvido que o JavaScript não é bloqueador, 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, sem bloquear o principal thread 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.
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 design.
Como alguém cria um padrão? Digamos que você reconheceu um problema comum e tem sua própria solução exclusiva para esse problema, que não é reconhecida e documentada globalmente. Você usa essa solução sempre que encontra esse problema e acha que ela pode ser reutilizada e que a comunidade de desenvolvedores pode se beneficiar com isso.
Isso se torna imediatamente um padrão? Felizmente, 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 algo quando, na verdade, não é um padrão.
Como saber quando o que você acha que reconhece é na verdade um padrão de design?
Obtendo as opiniões de outros desenvolvedores sobre isso, sabendo sobre o processo de criação de um padrão em si e familiarizando-se bem 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 protopadrão é um futuro padrão E se ele passa por um certo período de teste por vários desenvolvedores e cenários onde o padrão prova ser ú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 a fim de fazer um padrão completo reconhecido pela comunidade.
Como um padrão de projeto representa uma boa prática, um antipadrão representa uma prática ruim.
Um exemplo de um antipadrão seria modificar o Object
protótipo de classe. Quase todos os objetos em JavaScript herdam de Object
(lembre-se de que o JavaScript usa herança baseada em protótipo), então imagine um cenário em que você alterou esse protótipo. Alterações em Object
protótipo seria visto em todos os objetos que herdam deste protótipo - qual seria a maioria 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.
Da mesma forma que é útil saber sobre todas as boas práticas e soluções, também é muito importante saber sobre as ruins. Dessa forma, você pode reconhecê-los e evitar cometer erros logo no início.
Os padrões de design podem ser categorizados de várias maneiras, mas a mais popular é a seguinte:
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. Os padrões de projeto criacionais resolvem esse problema controlando de alguma forma a criação de objetos. Alguns dos padrões de design populares nesta categoria são:
Esses padrões lidam com relacionamentos de objeto. Eles garantem que se uma parte de um sistema muda, todo o sistema não precisa mudar junto com ele. Os padrões mais populares nesta categoria são:
Esses tipos de padrões reconhecem, implementam e melhoram a comunicação entre objetos distintos em um sistema. Eles ajudam a garantir que partes díspares de um sistema tenham informações sincronizadas. Exemplos populares desses padrões são:
Esses tipos de padrões de projeto lidam com paradigmas de programação multithread. Alguns dos mais populares são:
Padrões de design que são usados para fins arquitetônicos. Alguns dos mais famosos são:
Na seção a seguir, daremos uma olhada mais de perto em alguns dos padrões de design mencionados acima, com exemplos fornecidos para melhor compreensão.
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 melhor ajuste. Precisamos aprender quando um determinado padrão se mostrará útil e se ele fornecerá valor real. Assim que estivermos familiarizados com os padrões e cenários para os quais eles são mais adequados, podemos facilmente determinar se um padrão específico é ou não adequado para um determinado problema.
Lembre-se de que aplicar o padrão errado a um determinado problema pode levar a efeitos indesejáveis, como complexidade desnecessária do código, sobrecarga desnecessária no desempenho ou até mesmo o surgimento 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.
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:
// 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();
Depois de 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 forma 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 oferece suporte a classes nativas, mas oferece suporte a construtores por meio do uso de uma 'nova' palavra-chave prefixada a uma chamada de função. Dessa forma, podemos usar a função como um construtor e inicializar suas propriedades da mesma forma que faríamos com um construtor de linguagem clássica.
// 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 o 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 de Person
construtor. 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) 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 de Person
o construtor pode acessar uma instância compartilhada do writesCode()
método.
No que diz respeito às peculiaridades, o JavaScript nunca para de surpreender. Outra coisa peculiar ao JavaScript (pelo menos no que diz respeito às linguagens orientadas a objetos) é que o JavaScript não oferece suporte a 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 oferece suporte a classes nem modificadores de acesso, os desenvolvedores de JavaScript descobriram uma maneira de imitar esse comportamento quando necessário.
Antes de entrarmos nas especificações do padrão do módulo, vamos falar sobre o conceito de encerramento. UMA fecho é 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 por meio 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 de contador a uma função que foi chamada e fechada, mas ainda pode ser acessada pela função filha que a incrementa. Uma vez que não podemos acessar a variável do contador de fora da expressão da função, nós a tornamos privada por meio da manipulação do escopo.
Usando os fechamentos, podemos criar objetos com partes privadas e públicas. Estes são chamados módulos e são muito úteis sempre que queremos ocultar certas partes de um objeto e apenas expor uma interface para o 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 apresenta é a separação clara das partes privadas e públicas de um objeto, que é um conceito muito semelhante a desenvolvedores vindos de um histórico clássico de orientação 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.
Este 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, em seguida, 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 pelo menos 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 de módulo revelador é muito mais fácil de usar e modificar; no entanto, pode ser frágil em certos cenários, como o uso de objetos RMP como protótipos em uma cadeia de herança. As situações problemáticas são as seguintes:
O padrão singleton é usado em cenários quando precisamos de exatamente uma instância de uma classe. Por exemplo, precisamos ter um objeto que contém alguma configuração para algo. Nestes 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 observar 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 ele é bastante difícil de testar.
O padrão de observador é uma ferramenta muito útil quando temos um cenário em que precisamos melhorar a comunicação entre partes díspares de nosso sistema de forma otimizada. Ele promove o acoplamento fraco entre 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 relativas 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 de padrão 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 o editor / assinante promove um acoplamento ainda mais fraco 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 no padrão editor / assinante temos canais, que servem como ponte de comunicação entre um assinante e um editor. O editor dispara um evento e simplesmente executa a função de retorno de chamada enviada para esse evento.
Vou mostrar um breve exemplo do padrão de editor / assinante, mas para os interessados, um exemplo de padrão de observador clássico 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 realizar várias operações em um único evento que está sendo disparado. 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, executar 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 inferno de retorno de chamada. Usar o padrão editor / assinante é uma solução muito mais elegante.
Uma desvantagem de usar esse padrão é o teste difícil de várias partes de nosso sistema. Não existe uma maneira elegante de sabermos se as partes do sistema de assinatura estão se comportando conforme o esperado.
Cobriremos brevemente um padrão que também é muito útil quando falamos sobre 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 apresentar um mediador.
o que são vazamentos de memória em java
Um mediador é um objeto que é usado como um ponto central para a comunicação entre partes distintas de um sistema e controla 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 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 permitir que os assinantes corretos saibam quais eventos foram disparados. O agregador de eventos não se importa com o que acontece depois que um evento foi disparado, o que não é o caso com 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 no qual trabalhou. Frequentemente, quando muitas informações são exigidas de um usuário, é uma boa prática dividir isso em várias etapas.
Desta forma, o código ficará muito mais limpo (mais fácil de manter) e o usuário não se atrapalhará com a quantidade de informações que é solicitada apenas para finalizar o cadastro. Um mediador é um objeto que trataria das etapas de registro, levando em consideração os diferentes fluxos de trabalho possíveis que poderiam ocorrer devido ao fato de que cada usuário poderia potencialmente 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, o que significa que se nosso mediador falhar, todo o sistema pode parar de funcionar.
Como já mencionamos ao longo do artigo, JavaScript não oferece suporte a classes em sua forma nativa. A herança entre objetos é implementada usando programação baseada em protótipo.
Ele nos permite criar objetos que podem servir como um protótipo para outros objetos sendo criados. O objeto de protótipo é usado como um projeto 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();
Observe como a herança do protótipo aumenta o desempenho também, porque ambos os objetos contêm uma referência às funções que são implementadas no próprio protótipo, em vez de em cada um dos objetos.
O padrão de comando é útil nos casos em que desejamos desacoplar objetos que executam os comandos dos objetos que executam os comandos. Por exemplo, imagine um cenário em que nosso aplicativo está usando um grande número de chamadas de serviço de API. Então, digamos que os serviços de API mudem. Teríamos que modificar o código onde quer que as APIs que mudaram sejam chamadas.
Esse seria um ótimo lugar para implementar uma camada de abstração, que separaria os objetos que chamam um serviço de API dos objetos que os informam quando para chamar o serviço API. Desta forma, evitamos a modificação em todos os locais onde temos a necessidade de chamar o serviço, mas sim temos que alterar apenas os objetos que estão fazendo a própria chamada, que é apenas um local.
Como acontece com qualquer outro padrão, precisamos saber quando exatamente há uma necessidade real de tal padrão. Precisamos estar cientes da troca que estamos fazendo, pois estamos adicionando uma camada de abstração adicional sobre as chamadas de API, o que reduzirá o desempenho, mas potencialmente economizará muito tempo quando precisarmos modificar objetos que executam os comandos.
// 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));
O padrão de fachada é usado quando queremos criar uma camada de abstração entre o que é mostrado publicamente e o que é implementado por trás da cortina. É usado quando uma interface mais fácil ou mais simples para um objeto subjacente é desejada.
Um ótimo exemplo desse padrão seriam seletores de bibliotecas de manipulação DOM, como jQuery, Dojo ou D3. Você deve ter notado ao usar essas bibliotecas que elas têm recursos de seletor muito poderosos; você pode escrever em consultas complexas, como:
jQuery('.parent .child div.span')
Ele simplifica muito os recursos de seleção e, embora pareça simples na superfície, há toda uma lógica complexa implementada sob o capô para que isso funcione.
Também precisamos estar cientes da relação entre desempenho e simplicidade. É desejável evitar complexidade extra se não for benéfico o suficiente. No caso das bibliotecas citadas, a compensação valeu a pena, pois são todas bibliotecas de muito sucesso.
Os padrões de design são uma ferramenta muito útil que qualquer desenvolvedor sênior de JavaScript deve estar ciente. Saber os detalhes sobre os padrões de design pode ser extremamente útil e economizar muito tempo no ciclo de vida de qualquer projeto, especialmente na parte de manutenção. Modificar e manter sistemas escritos com a ajuda de padrões de design que são adequados para as necessidades do sistema pode ser inestimável.
Para manter o artigo relativamente breve, não exibiremos mais exemplos. Para os interessados, uma grande inspiração para este artigo veio do livro Gang of Four Padrões de projeto: elementos de software orientado a objetos reutilizáveis e Addy Osmani's Aprendendo Padrões de Design JavaScript . Eu recomendo fortemente os dois livros.
Relacionado: Como um desenvolvedor JS, é isso que me mantém acordado à noite / Fazendo sentido da confusão da classe ES6JavaScript é assíncrono, oferece suporte a funções de primeira classe e é baseado em protótipo.
Os padrões de design são soluções reutilizáveis para problemas comuns no design de software. São soluções comprovadas, facilmente reutilizáveis e expressivas. Eles diminuem o tamanho da sua base de código, evitam a refatoração futura e tornam o código mais fácil de entender por outros desenvolvedores.
JavaScript é uma linguagem de script do lado do cliente para navegadores criada inicialmente por Brendan Eich para o Netscape Navigator pelo que agora é o Mozilla.
ECMAScript é uma especificação de linguagem de script padronizada que todos os navegadores modernos tentam oferecer suporte. Existem várias implementações de ECMAScript, a mais popular das quais é JavaScript.
Um protopadrão é um futuro padrão se passar por um determinado período de teste por vários desenvolvedores e cenários em que o padrão se mostra útil e fornece resultados corretos.
Se um padrão de projeto representa uma boa prática, um antipadrão representa uma má prática. Um exemplo de um antipadrão seria a modificação do protótipo da classe Object. As alterações no protótipo de objeto podem ser vistas em todos os objetos que herdam desse protótipo (ou seja, quase todos os objetos em JavaScript).
Os padrões de design podem ser criativos, estruturais, comportamentais, simultâneos ou arquitetônicos.
Alguns exemplos discutidos no artigo acima incluem o padrão de construtor, o padrão de módulo, o padrão de módulo revelador, o padrão singleton, o padrão de observador, o padrão de mediador, o padrão de protótipo, o padrão de comando e o padrão de fachada.