#Entendendo Design Patterns em JavaScript #medio

Tempo de leitura: 13 minutos

Quando você inicia um novo projeto, não inicia imediatamente pela codificação. Primeiro você precisa definir os objetivos e escopos, em seguida, listar os recursos ou especificações. Logo depois ai é que o momento de você iniciar a codificação. Para tal tarefa é sempre bom escolher um padrão de pattern mais adequado e que se encaixe melhor ao seu projeto.

O que é um Design Pattern?

Na engenharia de software, um design pattern é uma solução reutilizável para problemas comuns no projeto de software. Design Patterns representam as melhores práticas usadas pelos desenvolvedores de software experientes. Um design pattern pode ser considerado como um modelo de programação.

Por que usar um Design Pattern?

Muitos programadores pensam que os Design Patterns são uma perda de tempo ou não sabem como aplicá-los adequadamente. Mas o uso de um design pattern apropriado pode ajudar você a escrever um código melhor e mais compreensível, e com isso ele se torna mais fácil de ser mantido porque é mais fácil de entender.

Mais importante ainda, os Design Patterns dão aos desenvolvedores de software um vocabulário comum para falar. Eles mostram a intenção do seu código instantaneamente para alguém que está aprendendo o código.

Por exemplo, se você estiver usando um pattern de decorador em seu projeto, um novo programador saberá imediatamente o que esse código está fazendo e poderá se concentrar mais na solução do problema de negócios em vez de tentar entender o que esse código está fazendo.

Agora que sabemos o que são os Design Patterns e por que eles são tão importantes, vamos mergulhar em vários Patterns usados em JavaScript.

Module Pattern

Um module Pattern é um código independente para que possamos atualizar o módulo sem afetar as outras partes do código. Os módulos também nos permitem evitar a poluição do namespace criando um escopo separado para nossas variáveis. Também podemos reutilizar módulos em outros projetos quando eles são desacoplados de outros trechos de código.

Os módulos são parte integrante de qualquer aplicativo JavaScript moderno e ajudam a manter nosso código limpo, separado e organizado.

Há muitas maneiras de criar módulos em JavaScript, uma dos quais aqui falada é atravéz do module pattern.

Ao contrário de outras linguagens de programação, o JavaScript não possui modificadores de acesso, ou seja, você não pode declarar uma variável como privada ou pública. Portanto, o module pattern também é usado para emular o conceito de encapsulamento. Esse padrão usa IIFE (expressão de função chamada imediatamente), closures e escopo de função para simular esse conceito.

Por exemplo:

 const myModule = (function() {

  const privateVariable = 'Hello World';

  function privateMethod() {
    console.log(privateVariable);
  }
  return {
    publicMethod: function() {
      privateMethod();
    }
  }
})();
myModule.publicMethod();

Como é IIFE, o código é imediatamente executado e o objeto retornado é atribuído à variável myModule. Devido a encerramentos, o objeto retornado ainda pode acessar as funções e variáveis definidas dentro do IIFE, mesmo após o término do IIFE.

Portanto, as variáveis e funções definidas dentro do IIFE são essencialmente ocultas do escopo externo e, portanto, tornando-o privado para a variável myModule. Depois que o código é executado, a variável myModule fica assim:

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

Assim, podemos chamar o publicMethod() que, por sua vez, chamará o privateMethod(). Por exemplo:

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

Revealing Module Pattern

O pattern Revealing Module é uma versão ligeiramente melhorada do módule pattern de Christian Heilmann. O problema com o módule pattern é que temos que criar novas funções públicas apenas para chamar as funções e variáveis privadas. Nesse pattern, mapeamos as propriedades do objeto retornado para as funções privadas que queremos revelar como públicas. É por isso que é chamado de Revealing Module Pattern. Por 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();
  }
  /** reveal methods and variables by assigning them to object     properties */
return {
    setName: publicSetName,
    greeting: publicVar,
    getName: publicGetName
  };
})();
myRevealingModule.setName('Mark');
// prints Name: Mark
myRevealingModule.getName();

Esse pattern facilita a compreensão de quais de nossas funções e variáveis podem ser acessadas publicamente, o que ajuda na legibilidade do código. Depois que o código é executado, o myRevealingModule fica assim:

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

Podemos chamar myRevealingModule.setName(‘Mark’), que é uma referência ao publicSetName interno e myRevealingModule.getName(), que é uma referência ao publicGetName interno. Por exemplo:

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

Vantagens do pattern Revealing Module sobre o Module Pattern:

  • Podemos mudar membros público para privado e vice-versa, modificando uma única linha na declaração de retorno.

  • O objeto retornado não contém nenhuma definição de função, todas as expressões do lado direito são definidas dentro do IIFE, tornando o código claro e fácil de ler.

ES6 Modules

Antes do ES6, o JavaScript não tinha módulos internos, por isso os desenvolvedores precisavam depender de bibliotecas de terceiros ou do module pattern para implementar os módulos. Mas com o ES6, o JavaScript tem módulos nativos. Os módulos do ES6 são armazenados em arquivos. Só pode haver um módulo por arquivo. Tudo dentro de um módulo é privado por padrão. Funções, variáveis e classes são expostas usando a palavra-chave export. O código dentro de um módulo sempre é executado no modo estrito.

Exporting a Module

Existem duas maneiras de exportar uma declaração de função e variável:

  • Adicionando a palavra-chave export na frente da declaração de função e 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;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
  • Adicionando a palavra-chave export no final do código contendo nomes de funções e variáveis que queremos exportar.

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;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
export {multiply, divide};

Importing a Module

Semelhante à exportação de um módulo, existem duas maneiras de importar um módulo usando a palavra-chave import.

Por exemplo:

  • Importando vários itens de uma só vez
// main.js
// importing multiple items
import { sum, multiply } from './utils.js';
console.log(sum(3, 7));
console.log(multiply(3, 7));

Importando tudo de um módulo

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

Importações e Exportações podem ser alias

Se você quiser evitar colisões de nomenclatura, poderá alterar o nome da exportação durante a exportação e também na importação. Por exemplo:

  • Renomeando uma exportação
// 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};
  • Renomeando uma importação
// main.js
import { add, multiply as mult } from './utils.js';
console.log(add(3, 7));
console.log(mult(3, 7));

Singleton Pattern

Um Singleton é um objeto que só pode ser instanciado apenas uma vez. Um singleton pattern cria uma nova instância de uma classe, se ela não existir. Se uma instância existir, ela simplesmente retorna uma referência a esse objeto. Quaisquer chamadas repetidas para o construtor sempre buscarão o mesmo objeto.

O JavaScript sempre teve singletons embutidos no idioma. Nós apenas não os chamamos de singletons, nós os chamamos de literal de objeto.

Por exemplo:

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

Como cada objeto em JavaScript ocupa um local de memória exclusivo e quando chamamos o objeto de user, retornamos essencialmente a referência a esse objeto.

Se tentarmos copiar a variável user para outra variável e modificar essa variável. Por exemplo:

const user1 = user;
user1.name = 'Mark';

Veríamos que ambos os objetos são modificados porque os objetos em JavaScript são passados por referência e não por valor. Portanto, há apenas um único objeto na memória. Por exemplo:

// prints 'Mark'
console.log(user.name);
// prints 'Mark'
console.log(user1.name);
// prints true
console.log(user === user1);

O singleton pattern pode ser implementado usando a função construtora.

Por exemplo:

let instance = null;
function User() {
  if(instance) {
    return instance;
  }
  instance = this;
  this.name = 'Peter';
  this.age = 25;

  return instance;
}
const user1 = new User();
const user2 = new User();
// prints true
console.log(user1 === user2); 

Quando essa função construtora é chamada, ela verifica se o objeto instance existe ou não. Se o objeto não existir, ele atribuirá a variável this à variável de instância. E se o objeto existe, apenas retorna esse objeto. Singletons também podem ser implementados usando o module pattern.

Por exemplo:

const singleton = (function() {
  let instance;

  function init() {
    return {
      name: 'Peter',
      age: 24,
    };
  }
  return {
    getInstance: function() {
      if(!instance) {
        instance = init();
      }

      return instance;
    }
  }
})();
const instanceA = singleton.getInstance();
const instanceB = singleton.getInstance();
// prints true
console.log(instanceA === instanceB);

No código acima, estamos criando uma nova instância chamando o método singleton.getInstance. Se uma instância já existir, esse método simplesmente retornará essa instância; se a instância não existir, ela criará uma nova instância chamando a função init().

Factory Pattern

Factory Pattern é um padrão que usa métodos de fábrica para criar objetos sem especificar a classe exata ou função de construtor a partir da qual o objeto será criado.
O Factory Pattern é usado para criar objetos sem expor a lógica de instanciação. Esse padrão pode ser usado quando precisamos gerar um objeto diferente, dependendo de uma condição específica. 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);
      }
  }
}

Aqui eu criei uma classe Car e outra Truck (com alguns valores padrão) que é usada para criar novos carros e caminhões. E eu defini uma classe VehicleFactory para criar e retornar um novo objeto baseado na propriedade vehicleType recebida no objeto options.

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'
});
// Prints Car {doors: 4, state: "Brand New", color: "silver"}
console.log(car);
// Prints Truck {doors: 2, state: "used", color: "white"}
console.log(truck);

Eu criei uma nova fábrica de objetos da classe VehicleFactory. Depois disso, podemos criar um novo objeto Car ou Truck chamando factory.createVehicle e passando um objeto options com uma propriedade vehicleType com um valor de carro ou caminhão.

Decorator Pattern

Um Decorator Pattern é usado para estender a funcionalidade de um objeto sem modificar a classe existente ou função construtora. Esse padrão pode ser usado para adicionar recursos a um objeto sem modificar o código subjacente usando-os.

Um exemplo simples desse padrão seria:

function Car(name) {
  this.name = name;
  // Default values
  this.color = 'White';
}
// Creating a new Object to decorate
const tesla= new Car('Tesla Model 3');
// Decorating the object with new functionality
tesla.setColor = function(color) {
  this.color = color;
}
tesla.setPrice = function(price) {
  this.price = price;
}
tesla.setColor('black');
tesla.setPrice(49000);
// prints black
console.log(tesla.color);

Um exemplo mais prático desse padrão seria:

Digamos que o custo de um carro seja diferente dependendo do número de recursos que ele possui. Sem padrão de decorador, teríamos que criar classes diferentes para diferentes combinações de recursos, cada um com um método de custo para calcular o custo.

Por exemplo:

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

Mas com o Decorator Pattern, podemos criar uma classe base Car e adicionar o custo de diferentes configurações ao seu objeto usando as funções de decorador.

Por exemplo:

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

Primeiro, criamos uma classe base Car para criar os objetos Car. Em seguida, criamos o decorador para o recurso que queremos adicionar e passamos o objeto Car como um parâmetro. Em seguida, sobrescrevemos a função de custo desse objeto, que retorna o custo atualizado do carro e adiciona uma nova propriedade a esse objeto para indicar qual recurso foi adicionado. Para adicionar um novo recurso, poderíamos fazer algo assim:

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

No final, podemos calcular o custo do carro assim:

// Calculating total cost of the car
console.log(car.cost());

Conclusão

Falamos aqui neste post sobre alguns design patterns usados em JavaScript, existem vários patterns que não foram abordados aqui, que tambem podem ser implementados em JavaScript.

Embora seja importante conhecer os design patterns, também é importante não usá-los em excesso. Antes de usar um design patterns, você deve considerar cuidadosamente se o problema se encaixa nesse design pattern ou não.

Para saber se um padrão se ajusta ao seu problema, você deve estudá-lo, bem como as aplicalidades deste pattern.

Gostou do conteúdo? não deixe de seguir a uebile nas redes sociais, pois toda semana tem post novo aqui no blog com mais dicas para o seu impulso digital.