DEV Community

Cover image for Design Patterns no Javascript
Priscila Andreani
Priscila Andreani

Posted on

Design Patterns no Javascript

O que são Design Patterns?

Design Patterns são soluções reutilizáveis para problemas comuns no desenvolvimento de software, popularizado por um grupo de engenheiros (Gang of Four) de C++ por meio do livro “Design Patterns: Elements of Reusable Object-Oriented Software", 1994.

É importante destacar que Design Patterns não são algorítimos ou implementações específicas, funcionam mais como ideias, opiniões e abstrações de código. Por isso, a sua implementação depende de diversos fatores. É importante compreendermos os conceitos por trás desses padrões e como eles podem nos ajudar a encontrar uma melhor solução para o problema.

Podemos categorizar os padrões em três principais grupos:

  • Criacionais:

    Padrões de criação de objetos, aumentando a flexibilidade e reutilização dos códigos.

  • Estruturais:

    Padrões que explicam como montar objetos e classes em estruturas maiores, mantendo essas estruturas flexíveis e eficientes

  • Comportamentais:

    Padrões que se preocupam com algorítimos e atribuição de responsabilidades entre objetos.

Mas… e os padrões no JavaScript?

O Javascript é uma linguagem baseada em protótipos e, como mencionado, os Design Patterns foram criados pensados na programação orientada a objetos (POO) baseada em classes. Isso traz peculiaridades para as implementações de padrões no JS, como veremos neste post.

📌 A programação baseada em protótipos é um estilo de programação que constrói sua reutilização de comportamentos (a herança) através de um processo de reuso de objetos já existentes que funcionam como protótipos. Veja mais aqui.

⚠️ Aqui temos um ponto de atenção: por ser baseada em protótipos, alguns autores não consideram que o Javascript pode ser uma linguagem orientada objetos, mesmo com a adição de classes no ES6.

Por que utilizar Design Patterns no JavaScript?

Ao compreender e aplicar padrões, os desenvolvedores conseguem produzir códigos mais limpos e eficientes, enfrentando desafios complexos com eficácia. Design patterns fornecem estruturas, melhoram a organização, a capacidade de manutenção e promovem a reutilização de código.

Vamos explorar alguns dos padrões mais utilizados:

Module Pattern

Se você está iniciando no uso do JavaScript recentemente, especialmente após a versão ES5, este padrão pode parecer um pouco óbvio, mas não era tão comum no JS há um tempo atrás.

Um módulo refere-se a um arquivo contendo código JavaScript que expõe determinados valores, proporcionando o encapsulamento de código. Isso significa que os valores privados dentro do módulo não podem ser modificados por padrão. Somente os valores explicitamente marcados para exportação com a palavra-chave export são acessíveis a outros arquivos.

// private
const secret = 'mySecret'

// public
export function sum(x, y) {
  return x + y;
}

// public
export function multiply(x, y) {
  return x * y;
}
Enter fullscreen mode Exit fullscreen mode

Singleton Pattern

O Singleton é um padrão criacional empregado para assegurar que uma classe tenha apenas uma instância e que esta seja imutável. Ou seja, o Singleton pattern consiste na criação de um objeto que não pode ser copiado ou modificado. É muito utilizado quando precisamos de uma “fonte de verdade” para nossa aplicação.

⚠️ A partir do ES2015 os módulos são Singletons por padrão, tornando esse padrão menos necessário.

Vejamos alguns exemplos:

// >= ES2015 utilizando classes
let instance;
// 1. Criamos a classe `DBConnection` que contém os métodos `constructor`, `connect` e `disconnect`.
// No `constructor` verificamos se a classe já foi instanciada, em caso positivo, retornamos um erro.
class DBConnection {
    constructor(uri){
        if(instance){
            throw new Error("You can only create on single DB connection")
        }
        this.uri =. uri;
        instance = this;
    }

    connect() {
        console.log(`Db ${this.uri} has been connected`);
    }

    disconnect() {
        console.log('Db disconnected')
    }

}

// 2. Garantimos que a nossa variável seja imutável utilizando o método nativo `Object.freeze`
const connection = Object.freeze(new DbConnection('mongodb://...'))

// 3. Exportamos a variável como `default` para garantir seu acesso global.
export default connection; 
Enter fullscreen mode Exit fullscreen mode

Também podemos criar objetos sem utilizar uma class, deixando o código muito mais simples e limpo:

// JS < ES5 
// 1. Criamos o objeto `dbConnection` que contém os métodos `connect` e `disconnect`.
const dbConnection = uri => Object.freeze({
    uri, 
    connect: () => console.log(`Db ${this.uri} has been connected`);
    disconnect: () => console.log('Db disconnected')
})

// 2. Garantimos que a nossa variável seja imutável utilizando o método nativo `Object.freeze`
const connection = dbConnection('mongodb://...')

// 3. Exportamos a variável como `default` para garantir seu acesso global.
export default connection; 
Enter fullscreen mode Exit fullscreen mode

Factory Method Pattern

O padrão Factory é vantajoso quando lidamos com objetos que podem sofrer alterações após a criação. Ele concentra a lógica de criação em um único ponto, proporcionando simplificação e melhor organização do código.

Esse padrão se torna especialmente valioso em linguagens que não têm uma base orientada à criação de objetos, tornando desafiador gerar várias instâncias do mesmo tipo. Felizmente, no JavaScript podemos simplesmente utilizar uma arrow function ou uma função regular que retorne um objeto.

const createBug = (name, phrase) =>({
    name,
    phrase,
    species: 'insect',
    fly: () => console.log('ZzzZzzz!')
})

const bug1 = createBug("Ladybird", "I'm a red Ladybird")
console.log(bug1.name) // output: "Ladybird"
console.log(bug1.phrase) // output: "I'm a red Ladybird!"
console.log(bug1.fly()) // output: "ZzzZzzz!"
Enter fullscreen mode Exit fullscreen mode

Prototype Pattern

Esse é um padrão que provavelmente você já utilizou muitas vezes codando com JavaScript, embora talvez não tenha percebido que se trata de um padrão em si. Conforme mencionado na introdução deste post, o JavaScript é uma linguagem orientada a protótipos, ou seja, quase tudo é criado por meio de um protótipo. Entenda mais aqui.

Esse padrão é utilizado para compartilhar propriedades entre diversos objetos do mesmo tipo. Para isso, empregamos classes, já que tudo contido no corpo da classe é considerado parte do protótipo. O construtor, por sua vez, é individual, representando o único objeto que será retornado.

Então, quando temos uma funcionalidade que é compartilhada entre vários objetos, essa funcionalidade deve ser definida no corpo da classe. Vejamos:

class Dog {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  bark() {
    console.log(`${this.name} is barking!`);
  }
  wagTail() {
    console.log(`${this.name} is wagging their tail!`);
  }
}

const dog1 = new Dog('Max', 4);

console.log({ dog1 })
// Dog { name: 'Max', age: 4 },

console.log(dog1.bark())
// Max is barking!
Enter fullscreen mode Exit fullscreen mode

Os métodos bark e wagTail estão acessíveis no protótipo da classe Dog, sendo compartilhados por todos os objetos pertencentes a essa classe. Ou seja, todas as instâncias da classe têm automaticamente acesso a esses métodos, eliminando a necessidade de alocação específica de memória para cada uma delas.

console.log(dog1.__proto__)
/**
{
    bark: f bark(),
    constructor: class Dog,
    wagTail: f wagTail()
    [[Prototype]]: Object
}
*/
Enter fullscreen mode Exit fullscreen mode

Proxy Pattern

O Proxy é um padrão estrutural que permite que você utilize um substituto para outro objeto. Ou seja, em vez de acessar diretamente uma classe, empregamos um "intermediário" com a mesma interface da classe original, responsável por controlar o acesso a essa classe principal.

No JS podemos facilmente criar um novo proxy utilizando o método nativo Proxy (doc):

async function fetchCharacterFromAPI(id) {
    try {
        const character = await axios.get(`https://rickandmortyapi.com/api/character/${id}`);
        return character.data
    }
    catch (e) {
        console.log(e)
        throw e;
    }
}

const characterCache = {}
const cacheHandler = {
    get: async(target, id) = {
        // Se personagem já exista no cache, vai retornar a informação do cache
        if(target[id]{
            return target[id]
        }

    // Se não, busca da API
    const character = await fetchCharacterFromAPI(id);
    // Salva no cache
    characterCache[id] = character

    return character
    }
}

const getCharacter = new Proxy(characterCache, cacheHandler) 

// A primeira resposta vem da API
const characterAPI = await getCharacter[1]

console.log(characterAPI)

// A segunda resposta vem do Cache
const characterCache = await getCharacter[1]

console.log(characterCache)

Enter fullscreen mode Exit fullscreen mode

Observer Pattern

Utilizamos o padrão comportamental Observer para notificar seus “subscribers” que um evento ocorreu. Podemos fazer uma analogia com os canais de youtube que avisam seus inscritos através de notificações que um novo vídeo foi postado.

const observables = []
const observable = {
    subscribe: (func) => observables.push(func),
    notify: (data) => observables.forEach((observer) => observer(data)),
}

export default Object.freeze(observable);
Enter fullscreen mode Exit fullscreen mode
import Observable from'./observable';

export function sendToGoogleAnalytics(data){
    console.log('Sent to GA:', data)
}

export function sendToCustomAnalytics(data){
    console.log('Sent to CA:', data)
}

Observable.subscribe(sendToGoogleAnalytics);
Observable.subscribe(sendToCustomAnalytics);
Enter fullscreen mode Exit fullscreen mode
import Observable from'./observable';

const pinkBtn = document.getElementById('pink-btn');
const blueBtn = document.getElementById('blue-btn');

// Todas as vezes que o pinkBtn for clicado, todos os subscribers serão notificados
pinkBtn.addEventListener('click', () => {
    const data = 'Pink btn clicked';
    Observable.notify(data)
})

// Todas as vezes que o blueBtn for clicado, todos os subscribers serão notificados
blueBtn.addEventListener('click', () => {
    const data = 'Pink btn clicked';
    Observable.notify(data)
})
Enter fullscreen mode Exit fullscreen mode

Conclusão

É importante analisar cuidadosamente o problema a ser resolvido para compreender quando a aplicação de um padrão específico será benéfica e verdadeiramente agregará valor ao nosso código. A aplicação equivocada de um padrão pode resultar em efeitos indesejados, como complexidade desnecessária, perda de performance e até a criação de um anti-padrão.

Destaquei alguns dos 23 padrões documentados pelo GoF que considero particularmente úteis no dia-a-dia. Caso você tenha interesse em se aprofundar ainda mais no assunto, recomendo a leitura do livro atualizado sobre o assunto “Learning Javascript Design Patterns”, escrito pelo Addy Osmani, que também foi uma das minhas fontes de consulta para este artigo. Além disso, o Refactoring Guru é uma excelente fonte para explorar conceitos adicionais de programação e design de software.

Este é o primeiro artigo que escrevo baseado nas minhas anotações de estudo. Fique à vontade para expressar suas impressões e sugestões, pois seu feedback será muito importante. Pretendo em breve trazer um novo post para Design Patterns no React. Seguimos aprendendo juntos, até breve!


Referências:

Patterns.dev

Design Patterns - Refactoring Guru*

Learning Javascript Design Patterns - Addy Osmani

JavaScript Design Patterns – Explained with Examples - Germán Cocca

A Tour of JavaScript & React Patterns - Lydia Hallie

Top comments (0)