DevSecOps

9 mar, 2020

Design Patterns com JavaScript & TypeScript

100 visualizações
Publicidade

Os Design Patterns fazem parte do dia-a-dia de uma pessoa desenvolvedora de software quer ela queira ou não, muitas vezes nem sabemos, mas eles estão presentes em nossas vidas e vão continuar por muito tempo! Na grande parte das vezes podemos estar até utilizando esses padrões e nem sequer sabemos!

Neste artigo vamos aprender a identificar, dar nome e utilizar cada um dos padrões mais comuns – porque eles são
muitos e não vamos conseguir cobrir todos neste artigo – para que você já possa sair desenvolvendo e utilizando as
melhores práticas desde o início! Mas antes, o que são Design Patterns?

O que são design Patterns?

Se você é um desenvolvedor ou desenvolvedora, com certeza já ouviu falar na palavra Design Pattern não é mesmo? Os Padrões de Projeto (como costumamos chamar em português), são um conjunto de padrões e técnicas bem definidas para problemas comuns, que foram testadas e definidas nas últimas décadas por diversos desenvolvedores e testadas a exaustão em vários casos de uso.

Basicamente, utilizar um padrão destes te dá algum tipo de benefício quando estamos construindo nosso código. Este benefício pode ser uma manutenção mais rápida e simples, reutilização de código, facilidade de desenvolvimento, performance e muitos outros.

Programadores JavaScript devem ter achado este título um pouco estranho, porque os DP’s são fortemente
relacionados com conceitos da programação orientada à objetos (OOP). Então o que estamos fazendo tentando
descrever padrões de projetos para uma linguagem dinâmica como o JavaScript? Este é justamente o ponto!

Alguns padrões de projeto não são exclusivamente OOPs, portanto eles podem ser aplicados em qualquer local utilizando quaisquer linguagens! E, para aqueles padrões mais complexos de implementar em JavaScript, podemos recorrer ao TypeScript! Nosso grande aliado em transformar o JavaScript em uma linguagem tipada.

Originalmente tínhamos uma lista bem concisa no livro mais famoso sobre o tema de Padrões de projeto, o livro do GoF (Gang of Four), que tem este nome justamente porque seus quatro autores são considerados os precursores e os
maiores nomes da OOP até hoje. Os padrões descritos e empregados no livro ainda são usados até hoje, contudo, novos padrões estão sendo adicionados constantemente, o que faz com que as listas de padrões sempre estejam
desatualizadas. Estes novos padrões contemplam linguagens mais recentes como o JavaScript.

Todos os padrões são agrupados em classificações pelo livro do GoF, estas classificações são:
• Padrões estruturais (Structural Patterns)
• Padrões comportamentais (Behavioral Patterns)
• Padrões criacionais (Creational Patterns)

Não vamos dar muita atenção do porquê cada padrão tem este nome e nem como separar cada um, isto é para outro
artigo, vamos focar aqui nos principais padrões e os mais comuns utilizados.

Padrões mais comuns

Como dissemos anteriormente, não vamos focar em todos os padrões, porque não temos como descrever todos em um artigo pequeno, então vamos focar nos padrões de projeto que são mais utilizados e sua implementação em JavaScript e/ou em TypeScript.

Immediately Invoked Function Expressions (IIFE) {#immediately-invoked-function-expressions-iife }

Este primeiro padrão é considerado por muitos como uma funcionalidade nativa do JavaScript e não um padrão em si, o
que não é completamente errado, porém faz mais sentido considera-lo como um padrão, já que ele resolve um
problema bastante simples. A da definição de um escopo no JS.
Basicamente uma IIFE é uma função que é ao mesmo tempo definida e executada. Por conta de como os escopos no
JavaScript funcionam, uma IIFE pode ser utilizada para criar campos e propriedades privadas em classes. Inclusive, este
padrão é muito utilizado como um pré requisito para outros padrões mais complexos que vamos ver mais a frente.
Uma IIFE é basicamente isso:

(function () {
   const a = 10
   const b = 20
   const c = 30
   const res = a + b + c
   console.log(res)
})()

Se você copiar esta função e passar pelo seu browser ou até mesmo em um REPL do Node.js, vamos obter o resultado
na hora porque estamos ao mesmo tempo criando e executando a função. Além disso, podemos também passar
parâmetros para essas funções assim que as executamos:

(function (c) {
  const a = 10
  const b = 20
  const res = a + b + c
  console.log(res)
})(30)

Casos de uso

No geral podemos utilizar IIFEs para alguns casos bem simples.

Simular variáveis estáticas

Podemos simular variáveis estáticas, presentes no escopo geral das classes como um valor único de forma simples e
rápida:

let autoIncrement = (function() {
   let number = 0
   return function () {
      number++
      return number
   }
})()

O que temos aqui é basicamente o uso de closures e variáveis privadas, pois estamos retornando uma nova função que será alocada na variável autoIncrement, que por sua vez sempre terá acesso à referência da variável number.

Simular variáveis privadas

O JavaScript ainda não possui campos privados para classes, porém podemos simular essa funcionalidade de uma forma bastante rápida. E se ao invés de pegarmos diretamente o valor do número que incrementamos na função anterior, nós pudéssemos acessar uma propriedade interna?

let autoIncrement = (function() {
  let number = 0
  
  return {
    incr() {
      number++
    },
    get number() {
      return number
    }
  }
})()

Poderíamos chamar o código da seguinte forma:

autoIncrement.incr()
autoIncrement.incr()
autoIncrement.number // 2
autoIncrement.number = 3 // Vai nos dar um output de 3
autoIncrement.number // 2

Factory Method

O padrão Factory Method é um padrão criacional, ele é um dos patterns mais úteis em todos os casos, porque é simples de implementar e de entender e também porque permite que limpemos um pouco o nosso código e transformemos em algo mais reutilizável.

Basicamente, todos os padrões do tipo factory permitem que você centralize a lógica de criação de objetos em uma única classe fabrica (por isso factory). Essa lógica de criação de objetos pode ser, por exemplo, uma pré configuração ou então até mesmo escolher entre o polimorfismo de classes diferentes para saber qual objeto criar no momento e por que.

O factory method pode ser melhor explicado se olharmos em um exemplo real de uso em TypeScript:

function clientCode(creator: AbstractProductFactory) {
  console.log(
    "Client: Não tenho ideia do que é o Creator, mas vai dar certo"
  )
  console.log(creator.someOperation())
}
  
console.log("App: ConcreteCreator1.")
clientCode(new ConcreteCreator1())
console.log("")
  
console.log("App: ConcreteCreator2.")
clientCode(new ConcreteCreator2())

Veja que estamos abstraindo a lógica de criação de um objeto para uma classe chamada AbstractProductFactory, este tipo de classe é sempre abstrata, pois não podemos criar um AbstractProductFactory logo de cara, ele precisa ser estendido por algo concreto, por isso criamos nossas classes ConcreteProductFactory, cada um deles vai ser responsável por criar um tipo de produto a partir da mesma interface.

interface Product {
  operation(): string
}
  
abstract class AbstractProductFactory {
  public abstract factoryMethod(): Product
  public someOperation(): string {
    const product = this.factoryMethod()
    return `Factory: O mesmo código do Factory funcionou com ${product.operation()}`
  }
}

Agora vamos definir nossas classes concretas:

class ConcreteProduct1 implements Product {
  public operation(): string {
    return "Olá, eu sou o produto 1;
  }
}
  
class ConcreteProduct2 implements Product {
  public operation(): string {
    return "Olá eu sou o produto 2";
  }
}

Veja que o nosso AbstractProductFactory tem um método abstrato chamado factoryMethod, que será implementado por ambas as classes concretas que vamos implementar para criar nossos produtos:

class ConcreteProduct1Factory extends AbstractProductFactory {
  public factoryMethod(): Product {
    return new ConcreteProduct1();
  }
}
  
class ConcreteProduct2Factory extends AbstractProductFactory {
  public factoryMethod(): Product {
    return new ConcreteProduct2();
  }
}

Veja que agora estamos estendendo nossa classe para criar dois produtos diferentes a partir da mesma factory. Portanto nosso código executado será:

function clientCode(creator: AbstractProductFactory) {
  console.log(
    "Client: Não tenho ideia do que é o Creator, mas vai dar certo"
  )
  console.log(creator.someOperation())
}
  
console.log("App: ConcreteCreator1.")
clientCode(new ConcreteCreator1()) // Olá eu sou o produto 1
console.log("")
  
console.log("App: ConcreteCreator2.")
clientCode(new ConcreteCreator2()) // Olá eu sou o produto 2

Casos de uso

Vamos generalizar os casos de uso para algo mais útil pra gente, que tal uma forma de tratar a criação de objetos de erro? Por exemplo, imagina que tenhamos uma API que precise retornar erros diferentes baseados em classes diferentes de acordo com a mensagem do usuário e o tipo de retorno que vamos ter.

Se tivermos, digamos, uma API simples, com 3 endpoints, e cada endpoint puder retornar até 3 erros, vamos ter 9 linhas do tipo:

if (err) return res.json({code: 000, message: 'Error message'})

E a primeira regra da programação é nunca escrever a mesma linha mais de uma vez. Se precisássemos modificar o nosso objeto de erros teríamos que passar por todos os casos novamente um a um. Quando podemos simplesmente ter alguma coisa do tipo:

if (err) return res.json(ErrorFactory.getError(err))

Singleton

Vamos para o último padrão que vamos falar neste artigo. Ele também é um padrão criacional assim como o factory. Estamos falando do Singleton.

Esse é um padrão bastante conhecido e bastante antigo também, muitos consideram ele um anti-pattern, porém ele é bastante útil em diversos casos, principalemnte quando estamos utilizando bancos de dados.

Um singleton permite que você controle a quantidade de instâncias de uma classe que podem ser criadas. Na verdade, ele só permite que uma instância exista a qualquer momento – por isso chamamos de Singleton, do inglês, Single – na maioria das linguagens de programação isso é feito através da criação de uma propriedade estática na classe que mantém a contagem da quantidade de instâncias – ou até mesmo contém a intância criada – porém, como falamos anteriormente, não temos acesso a variáveis estáticas no JS, mas temos no TS, então vou implementar este padrão de diversas formas.

Vamos começar com o JS, temos dois meios para criar um singleton, a primeira é através de uma IIFE e a segunda através de um módulo ES6 usando uma variável local para guardar nossa instância, então quando exportamos o módulo, a classe toda é enviada mas a variável continua sendo global.

let instance = null
  
class Singleton {
  constructor () {
    this.prop = Math.random()
  }
  
  print () {
    console.log(this.prop)
  }
  
  static getInstance () {
    if (!instance) instance = new Singleton()
    return instance
  }
}
  
module.exports = Singleton

E ai podemos simplesmente utilizar da seguinte forma:

const Singleton = require('./singleton')
const obj = Singleton.getInstance()
const obj2 = Singleton.getInstance()
  
obj.printValue() // 0.003452789808
obj2.printValue() // 0.003452789808

E se compararmos obj === obj2 vamos obter true.

Agora vamos para a implementação em TypeScript, que é um pouco mais simples porque podemos simplesmente fazer a mesma coisa que fazíamos em outras linguagens de programação:

export default class Singleton {
  private static instance: Singleton
  private prop: number
  
  private constructor () {
    this.prop = Math.random()
  }
  
  public static getInstance (): Singleton {
    if (!Singleton.instance) Singleton.instance = new Singleton()
    return Singleton.instance
  }
  
  public printValue () {
    console.log(this.prop)
  }
}

Teríamos o mesmo resultado.

Casos de uso

O caso de uso mais comum para este padrão é também o problema mais clássico da computação moderna: bancos de dados.

Quando temos um gerenciador ou um SDK de bancos de dados, principalmente hoje quando trabalhamos em ambientes distribuídos utilizando containers e kubernetes, precisamos de uma aplicação stateless, ou seja, ela não pode manter o estado anterior, mas ao mesmo tempo precisamos de algo que seja eficiente para trazer nossos dados, e se criássemos uma conexão diferente para cada usuário de cada requisição do nosso sistema, com certeza teríamos um problema de eficiência.

Por isso, a grande maioria dos frameworks de bancos de dados, ORMs e outras coisas utilizam o padrão Singleton para retornar somente uma instância da conexão do banco, reaproveitando a mesma para todas as requisições daquela execução.

import driver from 'dbDriver'
export default class DBClass {
  private static instance: DBClass
  public readonly conn: Connection | null = null
  private props: PropsObj
  
  constructor (props) {
    this.props = props
    this.conn = this.connect()
  }
  
  private connect (): Connection {

    return driver.connect(this.props)
  }
  
  public static getInstance (): DBClass {
    if (!DBClass.instance) DBClass.instance = new DBClass()
    return DBClass.instance
  }
}

Conclusão

Vamos ainda entrar em mais detalhes sobre outros padrões de projeto em artigos futuros, vamos passar por todas as categorias e todos os padrões de projeto mais utilizados para que possamos entender parte a parte sobre o que eles tratam e como podemos implementar cada um deles usando ou JavaScript ou TypeScript (ou até ambos!).