Design Patterns em Javascript — Modularização e Orientação a objetos

Um pouco sobre modularização e orientação a objetos

Olá pessoas! Semana passada começamos nossa jornada para conhecer um pouco mais de Design Patterns em Javascript e se você ainda não leu o primeiro capítulo, clique aqui. Hoje veremos um pouco mais do básico antes de entrarmos de cabeça no coração dos Design Patterns. Lembrando que essa “saga” é apenas uma forma de documentação daquilo que eu estou estudando no momento, caso eu tenha explicado algum conceito de forma equivocada por favor comente.

Programação Modular

Programação modular é um software design usado para separar pequenas funcionalidades do programa em pequenos pedaços de código chamado de módulos.

Se fossemos desenvolver um carro virtual, por exemplo, cada uma de suas partes poderiam ser um módulo e cada módulo conteria apenas o código referente aquela parte em específica do carro. O módulo do capô conteria apenas código de importância ao capô do carro e nada mais. Assim, isolamos nosso código deixando-o mais simples e prático para realizar uma futura manutenção, além de contribuir com a legibilidade e com sua escalabilidade.

Acredito que programar não é sobre criar uma única peça de lego que representa todo seu software mas sim criar pequenas peças de lego que juntas criam um software em perfeita harmonia.

Carro e seus módulos

Carro e seus “módulos”.

Agora que já vimos o que é um módulo e suas vantagens, bora ver os principais patterns de criação de módulo no Javascript.


UMD

UMD (Universal Module Definition) é um pattern que identifica o ambiente em que o código está sendo executado e mantém o código isolado do resto.

Uma biblioteca que implementa a UMD pode ser um módulo, módulo de servidor ou adicionará uma palavra ao objeto global, caso não haja saída.

Implementação do UMD:

/*
O UMD verifica se você está usando AMD ou CommonJS.
Caso você não esteja usando nenhum desses dois, ele
adiciona seu módulo no objeto global.
*/
(function (global, factory) {
   // verifica se é AMD
   if (typeof define === "function" && define.amd) {
     define(["exports"], factory);
   // verifica se é CommonJS  
   } else if (typeof exports !== "undefined") {
     factory(exports);
   } else {
     // adiciono ao objeto global
     var mod = {
       exports: {}
     };
     // passo o mod.exports para o segundo parâmetro da IIFE que adicionará o que o módulo exportará
     // dentro de mod.exports
     factory(mod.exports);
     // adiciona o seu módulo que está dentro de mod.exports no objeto global.
     global.myCompanyModule = mod.exports;
   }
 })(this, function (exports) {
   'use strict';
   exports.default = 'abcde';
 });

//Agora veja seu módulo em window.myCompanyModule

No código acima vimos que o UMD verifica se o módulo é do tipo AMD ou CommonJS antes de adicionar o módulo no objeto global. Agora, vamos ver essas e outras formas de criar um módulo em Javascript.


Revealing Module Pattern

Com o Revealing Module Pattern podemos ter funções, variáveis públicas e privadas dentro do nosso módulo. Isso é importante porque muitas vezes não queremos expor todo o nosso código por N motivos.

Como o código fica dentro de uma IIFE, ela é executada no carregamento do script e o que é público fica dentro do objeto global.

Implementação do Revealing Module Pattern:

(function(global){
  const idade = 21;
  const cpf = 13412312321;
  function meuNomeQueSeraExposto() {
    return 'Lucas';
  }
  
  function meuNomeQueNaoSeraExposto() {
    return 'Lucas Muniz Dutra';
  }
  
  global.lucas = {
    nome: meuNomeQueSeraExposto,
    idade,
  }
}(this));

/*
no console:
lucas.nome // 'Lucas'
lucas.idade // 21
lucas.cpf // undefined pois não exportamos este dado
*/

Eu particularmente uso muito esse pattern em meus projetos pessoais.


AMD

AMD (Asynchronous Module Definition) é uma API Javascript que define módulos de uma forma que possam ser carregados de forma assíncrona, sem travar o carregamento do seu software e melhorando a performance da sua aplicação.

Uma das bibliotecas mais famosas que trabalham com AMD é o RequireJS.

O código do módulo não será executado até que suas dependências sejam resolvidas.

O primeiro parâmetro é o nome do módulo, o segundo é um array com todas as dependências e o terceiro é uma função que recebe todos os módulos por parâmetro. Dos três parâmetros, apenas o último é obrigatório.

Implementação do AMD:

define('meuModulo', ['lodash'], function(_) {
  // código do módulo
  return {
    ...
  };
});
 
 // os argumentos do módulo devem estar na mesma ordem do array de dependências
 define('meuModulo', ['lodash', 'facebook'], function(_, fb) {
  // código do módulo
  return {
    ...
  };
});
   
   
define(function() {
  // código do módulo
  return {
    ...
  };
});

CommonJS

O CommonJS é muito usado no Node.js. Os módulos são carregados síncronamente.

// carregando o lodash na variável _
const _ = require('lodash');

function meuModulo() {

}
// você pode retornar uma função, objeto ou uma variável.
// agora o meuModulo pode ser importado usando o require.
module.exports = meuModulo;

// exportando apenas uma função
//teste.js
exports.fn1 = function fn1() {

}

exports.fn2 = function fn2() {

}

//executando as funções
const fn1 = require('./teste').fn1();
const fn2 = require('./teste').fn2();

Orientação a objetos

Em linguagens de programação orientada a objetos normalmente temos quatro palavras que nos ajudam a trabalhar:

  • class: Define uma classe onde o programador pode especificar atributos e métodos que representam essa classe. Você sempre deve instanciar uma classe para usá-la.
  • interface: A interface contém apenas a assinatura do método e não sua implementação. Ela define um “contrato” em que a classe que implementar esta interface deve obrigatoriamente implementar os métodos definidos desta interface. Não implementada no JS.
  • extends: Usada para indicar que essa classe herda comportamentos e atributos de outra classe. A herança é sempre de cima pra baixo. Isso quer dizer que se a classe A herda de B, a classe B tem todos os métodos e atributos de A mas A não tem todos os métodos e atributos de B.
  • implements: Usada para indicar que a classe irá implementar todos os métodos de determinada interface. Não implementada no JS.

Em outras linguagens as classes são definições de objetos e quando ocorre uma instância com o new todo objeto é criado com seus comportamentos e métodos que são próprios ou herdados. Porém com Javascript a história é um pouco diferente… Tudo é um objeto e cada objeto tem um link interno para outro objeto chamado prototype. Esse objeto prototype tem um link para outro objeto prototype e assim por diante até que null seja encontrado por um prototype. Null que não tem prototype age como o final dessa cadeia de protótipos chamada de prototype chain.

Prototype chain:

/*
  navegaremos de construtor a construtor até obter um erro 
  no final na prototype chain
*/
var set = new Set();
console.log(set);
// []
console.log(set.constructor);
// ƒ Set() { [native code] }
console.log(set.__proto__.constructor);
// ƒ Set() { [native code] }
console.log(set.__proto__.__proto__.constructor);
// ƒ Object() { [native code] }
console.log(set.__proto__.__proto__.__proto__.constructor);
// Uncaught TypeError: Cannot read property 'constructor' of null

/*
  Como o temos dois construtores diferentes (Set e Object)
  podemos dizer que nosso set é uma instância de Set e de Object
*/

console.log(set instanceof Set) // true
console.log(set instanceof Object) // true

Agora, vamos ver como a herança prototipal funciona em Javascript:

Herança:

function Veiculo(nome, valor, cor) {
  this.nome = nome;
  this.valor = valor;
  this.cor = 'preto';
};

Veiculo.prototype.acelerar = function() {
  console.log('aceleraaaaaa');
}

function Ferrari() {
  // A "classe" Veiculo é chamada usando o contexto existente dentro de Ferrari... ou seja
  // todos os atributos da classe serão adicionados ao this de Ferrari
  Veiculo.call(this);
  this.marca = 'Ferrari';
}

Ferrari.prototype.acelereComoUmaFerrari = function() {
  console.log('ACELERAAAAAAAA FERRARI');
}

let f = new Ferrari();
f.acelereComoUmaFerrari(); // ACELERAAAAAAAA FERRARI
f.acelerar(); // TypeError: f.acelerar is not a function

O erro acima acontece porque acelerar foi adicionado ao prototype de Veiculo e não de Ferrari. Se você executar no seu console: Object.getOwnPropertyNames(Ferrari.prototype) verá que no prototype de Ferrari existe apenas a função acelereComoUmaFerrari que não existe em Veiculo e Object.getOwnPropertyNames(Veiculo.prototype) temos a função acelerar que não existe em Ferrari.

Precisamos de alguma forma fazer com que Ferrari() herde os métodos de Veiculo(). Herança dos métodos de Veiculo e reatribuição do constructor de Ferrari:

// agora a classe Ferrari extends Veiculo
Ferrari.prototype = Object.create(Veiculo.prototype);

// porém agora o constructor de Ferrari.prototype é igual
// ao de Veiculo.prototype... Precisamos resolver isso.
Ferrari.prototype.constructor = Ferrari;

let ff = new Ferrari();
ff.acelerar() // works!!
Ferrari.prototype.constructor // Ferrari

Orientação a objetos no ES6

Com a chegada do ES6 algumas keywords de linguagens baseadas em classe chegaram como um syntax sugar, apenas para facilitar a comunicação e o entendimento entre os desenvolvedores.

Classes em JavaScript são introduzidas no ECMAScript 2015 e são simplificações da linguagem para as heranças baseadas nos protótipos. A sintaxe para classes não introduz um novo modelo de herança de orientação a objetos em JavaScript. Classes em JavaScript provêm uma maneira mais simples e clara de criar objetos e lidar com herança.

- MDN

(Até eles sabem que trabalhar daquele jeito é difícil pra car**** xD)

Agora temos class para criação de classes, extends para lidar com a herança e static para nos auxiliar com métodos estáticos.

Que tal refatorar todo aquele código só que agora com a ajuda do ES6? Fica assim:

class Veiculo {
  constructor(nome, valor, cor) {
    this._nome = nome;
    this._valor = valor;
    this._cor = cor;
  }
  
  get cor() {
    return this._cor;
  }
  
  set cor(cor) {
    this._cor = cor;
  }
  
  static frear() {
    console.log('Freiando!!');
  }
  
  acelerar() {
    console.log('aceleraaaaaa');
  }
}

class Ferrari extends Veiculo {
  constructor() {
    super();
    this.marca = 'Ferrari';
  }
  
  acelereComoUmaFerrari() {
    console.log('ACELERAAAAAAAA FERRARI');
  }
}

let f = new Ferrari();
f.acelereComoUmaFerrari(); // works!
f.acelerar(); // works!
Ferrari.prototype.constructor; // Ferrari
f.cor = 'azul'; // works!
Veiculo.frear(); // works!
Ferrari.frear(); // works!

No código acima aproveitei também para implementar um exemplo get, set e um método estático.


Conclusão

Vimos alguns patterns para criação de módulos em Javascript e vale lembrar que isso é algo muito importante de se pensar desde o primeiro dia da criação de um produto. Existem muitas histórias de empresas com um enorme potencial de crescimento que simplesmente faliram porque tinham um software que era caro e complicado de dar manutenção. Sem falar na dor de cabeça para todo o time. Talvez você perca um tempinho definindo os patterns do seu projeto mas vale muito a pena!

Além disso, vimos que a herança prototipal de Javascript pode até ser meio confusa em comparação a outras linguagens (como Java e C++) mas com a chegada do ES6 ela se tornou um pouco mais intuitiva.

Comentários