Padrões de Design JavaScript

O autor do material, cuja tradução publicamos, diz que, ao iniciar um projeto, eles não começam imediatamente a escrever o código. Primeiro, eles determinam a meta e os limites do projeto, depois identificam as oportunidades que ele deve ter. Depois disso, eles escrevem imediatamente o código ou, se for um projeto bastante complexo, selecionam padrões de design adequados que formam sua base. Este material é sobre padrões de design do JavaScript. Ele foi desenvolvido principalmente para desenvolvedores iniciantes.



O que é um padrão de design?


No campo do desenvolvimento de software, um padrão de design é um projeto de arquitetura repetível que é uma solução para um problema de design em um contexto que geralmente surge. Os padrões de design são um resumo da experiência dos desenvolvedores profissionais de software. Um padrão de design pode ser considerado como um tipo de padrão de acordo com o qual os programas são gravados.

Por que os padrões de design são necessários?


Muitos programadores pensam que os padrões de design são uma perda de tempo ou simplesmente não sabem como aplicá-los corretamente. No entanto, o uso de um padrão adequado pode ajudar a escrever um código melhor e mais compreensível, que, devido à sua compreensibilidade, será mais fácil de manter.

A coisa mais importante aqui, talvez, é que o uso de padrões oferece aos desenvolvedores de software algo como um dicionário de termos conhecidos que são muito úteis, por exemplo, ao analisar o código de outra pessoa. Os padrões revelam o objetivo de certos fragmentos do programa para aqueles que estão tentando lidar com o dispositivo de um projeto.

Por exemplo, se você usar o padrão “Decorator”, ele informará imediatamente o novo programador que veio ao projeto sobre quais tarefas um determinado código resolve e por que é necessário. Graças a isso, esse programador poderá dedicar mais tempo às tarefas práticas que o programa resolve, em vez de tentar entender sua estrutura interna.

Agora que descobrimos o que são os padrões de design e para que servem, passamos aos próprios padrões e descrevemos sua implementação usando JavaScript.

Padrão "Módulo"


Um módulo é um trecho de código independente que pode ser alterado sem afetar outro código do projeto. Além disso, os módulos permitem evitar um fenômeno como a poluição das áreas de visibilidade devido ao fato de criarem áreas de visibilidade separadas para as variáveis ​​declaradas neles. Módulos criados para um projeto podem ser reutilizados em outros projetos, caso seus mecanismos sejam universais e não estejam vinculados às especificidades de um projeto específico.

Os módulos são parte integrante de qualquer aplicativo JavaScript moderno. Eles ajudam a manter a limpeza do código, ajudam a separar o código em fragmentos significativos e a organizá-lo. O JavaScript tem várias maneiras de criar módulos, um dos quais é o padrão "Módulo".

Diferente de outras linguagens de programação, o JavaScript não possui modificadores de acesso. Ou seja, as variáveis ​​não podem ser declaradas como privadas ou públicas. Como resultado, o padrão "Módulo" também é usado para emular o conceito de encapsulamento.

Esse padrão usa IIFE (expressão funcional chamada imediatamente), fechamentos e escopos de função para imitar esse conceito. Por exemplo:

const myModule = (function() {   const privateVariable = 'Hello World';   function privateMethod() {    console.log(privateVariable);  }  return {    publicMethod: function() {      privateMethod();    }  } })(); myModule.publicMethod(); 

Como temos o IIFE, o código é executado imediatamente e o objeto retornado pela expressão é atribuído à constante myModule . Devido ao fato de haver um fechamento, o objeto retornado tem acesso a funções e variáveis ​​declaradas dentro do IIFE, mesmo após o IIFE concluir seu trabalho.

Como resultado, as variáveis ​​e funções declaradas no IIFE estão ocultas dos mecanismos que estão no escopo de visibilidade externo a eles. Eles acabam sendo entidades privadas da constante myModule .

Após a execução desse código, o myModule ficará assim:

 const myModule = { publicMethod: function() {   privateMethod(); }}; 

Ou seja, referindo-se a essa constante, você pode chamar o método público do objeto publicMethod() , que, por sua vez, chamará o método privado privateMethod() . Por exemplo:

 //  'Hello World' module.publicMethod(); 

Padrão de módulo aberto


O padrão do módulo revelador é uma versão ligeiramente aprimorada do padrão do módulo que Christian Heilmann propôs. O problema com o padrão "Módulo" é que precisamos criar funções públicas apenas para acessar funções e variáveis ​​privadas.

Nesse padrão, atribuímos funções privadas às propriedades do objeto retornado que queremos tornar públicas. É por isso que esse padrão é chamado de “Módulo Aberto”. Considere um exemplo:

 const myRevealingModule = (function() { let privateVar = 'Peter'; const publicVar  = 'Hello World'; function privateFunction() {   console.log('Name: '+ privateVar); } function publicSetName(name) {   privateVar = name; } function publicGetName() {   privateFunction(); } /**    ,     */ return {   setName: publicSetName,   greeting: publicVar,   getName: publicGetName }; })(); myRevealingModule.setName('Mark'); //  Name: Mark myRevealingModule.getName(); 

A aplicação desse padrão facilita a compreensão de quais funções e variáveis ​​do módulo estão disponíveis publicamente, o que ajuda a melhorar a legibilidade do código.

Depois de executar o IIFE, o myRevealingModule fica assim:

 const myRevealingModule = { setName: publicSetName, greeting: publicVar, getName: publicGetName }; 

Podemos, por exemplo, chamar o myRevealingModule.setName('Mark') , que é uma referência à função interna publicSetName . O método myRevealingModule.getName() refere-se à função interna publicGetName . Por exemplo:

 myRevealingModule.setName('Mark'); //  Name: Mark myRevealingModule.getName(); 

Considere as vantagens do padrão "Módulo aberto" sobre o padrão "Módulo":

  • O "módulo aberto" permite tornar públicas entidades ocultas do módulo (e ocultá-las novamente, se necessário), modificando, para cada uma delas, apenas uma linha no objeto retornada após o IIFE.
  • O objeto retornado não contém uma definição de função. Tudo à direita de seus nomes de propriedades é definido no IIFE. Isso ajuda a manter o código limpo e fácil de ler.

Módulos no ES6


Antes do lançamento do padrão ES6, o JavaScript não tinha uma ferramenta padrão para trabalhar com módulos; como resultado, os desenvolvedores precisavam usar bibliotecas de terceiros ou o padrão "Módulo" para implementar os mecanismos apropriados. Mas com o advento do ES6, um sistema de módulo padrão apareceu em JavaScript.

Os módulos ES6 são armazenados em arquivos. Um arquivo pode conter apenas um módulo. Tudo dentro do módulo é privado por padrão. Funções, variáveis ​​e classes podem ser tornadas públicas usando a palavra-chave export . O código dentro do módulo é sempre executado no modo estrito.

▍ Módulo de exportação


Há duas maneiras de exportar uma função ou variável declarada em um módulo:

  • A exportação é feita adicionando a palavra-chave export antes de declarar uma função ou variável. Por exemplo:

     // utils.js export const greeting = 'Hello World'; export function sum(num1, num2) { console.log('Sum:', num1, num2); return num1 + num2; } export function subtract(num1, num2) { console.log('Subtract:', num1, num2); return num1 - num2; } //  -   function privateLog() { console.log('Private Function'); } 
  • A exportação é feita adicionando a palavra-chave export ao final do código, listando os nomes das funções e variáveis ​​a serem exportadas. Por exemplo:

     // utils.js function multiply(num1, num2) { console.log('Multiply:', num1, num2); return num1 * num2; } function divide(num1, num2) { console.log('Divide:', num1, num2); return num1 / num2; } //    function privateLog() { console.log('Private Function'); } export {multiply, divide}; 

ModuleMódulo de importação


Assim como existem duas maneiras de exportar, há duas maneiras de importar módulos. Isso é feito usando a palavra-chave import :

  • Importar vários itens selecionados. Por exemplo:

     // main.js //     import { sum, multiply } from './utils.js'; console.log(sum(3, 7)); console.log(multiply(3, 7)); 
  • Importe tudo o que o módulo exporta. Por exemplo:

     // main.js //  ,    import * as utils from './utils.js'; console.log(utils.sum(3, 7)); console.log(utils.multiply(3, 7)); 

▍ Aliases para entidades exportadas e importadas


Se os nomes das funções ou variáveis ​​exportadas para o código puderem causar uma colisão, elas poderão ser alteradas durante a exportação ou durante a importação.

Para renomear entidades durante a exportação, você pode fazer isso:

 // utils.js function sum(num1, num2) { console.log('Sum:', num1, num2); return num1 + num2; } function multiply(num1, num2) { console.log('Multiply:', num1, num2); return num1 * num2; } export {sum as add, multiply}; 

Para renomear entidades durante a importação, a seguinte construção é usada:

 // main.js import { add, multiply as mult } from './utils.js'; console.log(add(3, 7)); console.log(mult(3, 7)); 

Padrão Singleton


O padrão "Singleton" ou "Singleton" é um objeto que pode existir apenas em uma única cópia. Como parte da aplicação desse padrão, uma nova instância de uma classe é criada se ainda não tiver sido criada. Se a instância da classe já existir, ao tentar acessar o construtor, uma referência ao objeto correspondente será retornada. As chamadas subseqüentes ao construtor sempre retornarão o mesmo objeto.

De fato, o que chamamos de padrão "Singleton" sempre existiu no JavaScript, mas eles não o chamam de "Singleton", mas de "objeto literal". Considere um exemplo:

 const user = { name: 'Peter', age: 25, job: 'Teacher', greet: function() {   console.log('Hello!'); } }; 

Como cada objeto em JavaScript ocupa sua própria área de memória e não o compartilha com outros objetos, sempre que acessamos a variável de user , obtemos um link para o mesmo objeto.

O padrão Singleton pode ser implementado usando a função construtora. É assim:

 let instance = null; function User(name, age) { if(instance) {   return instance; } instance = this; this.name = name; this.age = age; return instance; } const user1 = new User('Peter', 25); const user2 = new User('Mark', 24); //  true console.log(user1 === user2); 

Quando a função construtora é chamada, ela primeiro verifica se o objeto de instance existe. Se a variável correspondente não for inicializada, this gravado na instance . Se a variável já tiver uma referência a um objeto, o construtor simplesmente retornará uma instance , ou seja, uma referência a um objeto existente.

O padrão Singleton pode ser implementado usando o padrão Module. Por exemplo:

 const singleton = (function() { let instance; function User(name, age) {   this.name = name;   this.age = age; } return {   getInstance: function(name, age) {     if(!instance) {       instance = new User(name, age);     }     return instance;   } } })(); const user1 = singleton.getInstance('Peter', 24); const user2 = singleton.getInstance('Mark', 26); // prints true console.log(user1 === user2); 

Aqui, criamos uma nova instância do user chamando o método singleton.getInstance() . Se uma instância do objeto já existir, esse método simplesmente o retornará. Se ainda não houver esse objeto, o método criará uma nova instância chamando a função construtora User .

Padrão de fábrica


O padrão de fábrica usa os chamados métodos de fábrica para criar objetos. Você não precisa especificar classes ou funções de construtor que são usadas para criar objetos.

Esse padrão é usado para criar objetos nos casos em que não é necessário tornar pública a lógica de sua criação. O padrão de fábrica pode ser usado se você precisar criar objetos diferentes, dependendo de condições específicas. Por exemplo:

 class Car{ constructor(options) {   this.doors = options.doors || 4;   this.state = options.state || 'brand new';   this.color = options.color || 'white'; } } class Truck { constructor(options) {   this.doors = options.doors || 4;   this.state = options.state || 'used';   this.color = options.color || 'black'; } } class VehicleFactory { createVehicle(options) {   if(options.vehicleType === 'car') {     return new Car(options);   } else if(options.vehicleType === 'truck') {     return new Truck(options);     } } } 

As classes Car e Truck são criadas aqui, que fornecem o uso de certos valores padrão. Eles são usados ​​para criar objetos de car e truck . A classe VehicleFactory também é declarada aqui, usada para criar novos objetos com base na análise da propriedade vehicleType , transmitida ao método correspondente do objeto que ele retorna no objeto com options . Veja como trabalhar com tudo isso:

 const factory = new VehicleFactory(); const car = factory.createVehicle({ vehicleType: 'car', doors: 4, color: 'silver', state: 'Brand New' }); const truck= factory.createVehicle({ vehicleType: 'truck', doors: 2, color: 'white', state: 'used' }); //  Car {doors: 4, state: "Brand New", color: "silver"} console.log(car); //  Truck {doors: 2, state: "used", color: "white"} console.log(truck); 

O objeto de factory da classe VehicleFactory é VehicleFactory . Depois disso, você pode criar objetos das classes Car ou Truck chamando o método factory.createVehicle() e passando a ele o objeto de options com a propriedade vehicleType configurada para car ou truck .

Padrão Decorador


O padrão Decorator é usado para estender a funcionalidade de objetos sem modificar classes existentes ou funções de construtor. Esse padrão pode ser usado para adicionar determinados recursos aos objetos sem modificar o código responsável por sua criação.

Aqui está um exemplo simples de uso desse padrão:

 function Car(name) { this.name = name; //    this.color = 'White'; } //   ,    const tesla= new Car('Tesla Model 3'); //   -    tesla.setColor = function(color) { this.color = color; } tesla.setPrice = function(price) { this.price = price; } tesla.setColor('black'); tesla.setPrice(49000); //  black console.log(tesla.color); 

Vamos agora considerar um exemplo prático da aplicação desse padrão. Suponha que o custo dos carros dependa de seus recursos, das funções adicionais disponíveis. Sem o uso do padrão Decorator, para descrever esses carros, teríamos que criar classes diferentes para combinações diferentes dessas funções adicionais, cada uma das quais teria um método para encontrar o custo de um carro. Por exemplo, pode ser assim:

 class Car() { } class CarWithAC() { } class CarWithAutoTransmission { } class CarWithPowerLocks { } class CarWithACandPowerLocks { } 

Graças ao padrão em questão, você pode criar um Car classe base, descrevendo, por exemplo, um carro na configuração básica, cujo custo é expresso por uma quantia fixa. Depois disso, o objeto padrão criado com base nessa classe pode ser expandido usando as funções do decorador. O “carro” padrão processado por essa função obtém novas oportunidades, o que, além disso, afeta seu preço. Por exemplo, este esquema pode ser implementado da seguinte maneira:

 class Car { constructor() { //   this.cost = function() { return 20000; } } } // - function carWithAC(car) { car.hasAC = true; const prevCost = car.cost(); car.cost = function() {   return prevCost + 500; } } // - function carWithAutoTransmission(car) { car.hasAutoTransmission = true;  const prevCost = car.cost(); car.cost = function() {   return prevCost + 2000; } } // - function carWithPowerLocks(car) { car.hasPowerLocks = true; const prevCost = car.cost(); car.cost = function() {   return prevCost + 500; } } 

Aqui, primeiro criamos a classe base Car , usada para criar objetos que representam carros como padrão. Em seguida, criamos várias funções decoradoras que nos permitem estender os objetos da classe Car base com propriedades adicionais. Essas funções tomam os objetos correspondentes como parâmetros. Depois disso, adicionamos uma nova propriedade ao objeto, indicando com qual novo recurso o carro será equipado e redefinimos a função de cost do objeto, que agora retorna o novo custo do carro. Como resultado, para "equipar" o carro de configuração padrão com algo novo, podemos usar o seguinte design:

 const car = new Car(); console.log(car.cost()); carWithAC(car); carWithAutoTransmission(car); carWithPowerLocks(car); 

Depois disso, você pode descobrir o custo do carro em uma configuração aprimorada:

 //       console.log(car.cost()); 

Sumário


Neste artigo, analisamos vários padrões de design usados ​​em JavaScript, mas, de fato, ainda existem muitos padrões que podem ser usados ​​para resolver uma ampla variedade de problemas.

Embora o conhecimento dos vários padrões de design seja importante para o programador, seu uso apropriado é igualmente importante. Conhecendo os padrões e o escopo de sua aplicação, o programador, analisando a tarefa à sua frente, pode entender que tipo de padrão é capaz de ajudar a resolvê-lo.

Caros leitores! Quais padrões de design você usa com mais frequência?

Source: https://habr.com/ru/post/pt427293/


All Articles