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');
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');
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);
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);
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?
