Artigo original: https://www.freecodecamp.org/news/four-pillars-of-object-oriented-programming/

O JavaScript é uma linguagem multiparadigma, ou seja, pode ser escrita seguindo diferentes paradigmas de programação. Um paradigma de programação é essencialmente um conjunto de regras que você segue ao escrever código, para ajudá-lo a resolver um problema específico.

No caso da Programação Orientada a Objetos, temos quatro pilares. Eles são princípios de design de software para ajudá-lo a escrever um código limpo e orientado a objetos.

Os quatro pilares da programação orientada a objetos são:

  • Abstração
  • Encapsulamento
  • Herança
  • Polimorfismo

Vamos dar uma olhada em cada um deles.

Abstração na Programação Orientada a Objetos

Abstrair algo significa esconder os detalhes da implementação dentro de algo – às vezes um protótipo, às vezes em uma função. Portanto, quando você chama a função, não precisa entender exatamente o que ela está fazendo.

Um exemplo claro do conceito de abstração seria o funcionamento de um carro. Quando acionamos ele para ligar, não precisamos saber quais passos ele faz para colocar o motor em funcionamento. Quando acionamos o freio, não precisamos saber todos os mecanismos que são acionados para fazer o carro frear. Apenas sabemos o que cada objeto ou função do carro produz como resultado.

Voltando para a codificação, se você tivesse que entender cada função em uma base de código grande, você nunca codificaria nada, pois, levaria meses para terminar de ler e entender a lógica de tudo isso.

Contudo, abstraindo certos detalhes, você é capaz de criar uma base de código reutilizável, simples de entender e facilmente alterável. Deixe-me lhe dar um exemplo:

function hitAPI(tipo){
	if (type instanceof CargaInicial) {
		// Exemplo de implementação
	} else if (type instanceof NavBar) {
		// Exemplo de implementação
	} else {
		// Exemplo de implementação
	}
}
Isso não é abstração de forma alguma.

Você percebeu nesse exemplo que você precisa especificar todos os passos da implementação de cada caso de uso?

Para cada novo tipo que você precisar acessar nessa função, você precisará de um novo bloco if e de seu próprio código personalizado. Isso não é um código abstraído, pois você precisa se preocupar com o passo a passo de cada implementação. Ele também não é reutilizável, sendo um pesadelo na hora de fazer a manutenção desse código.

Bom, e que tal o código abaixo?

hitApi('www.kealanparr.com', HTTPMethod.Get)

Desse modo, você simplesmente passou um URL para sua função e qual método HTTP deseja usar nela e está feito.

Você não precisa mais se preocupar com o funcionamento da função. Isso ajuda muito na reutilização de código, além de tornar seu código muito mais sustentável.

É disso que trata a Abstração. Encontrar coisas semelhantes em seu código e fornecer uma função ou objeto genérico e servir em vários lugares/com vários interesses.

Um bom exemplo para entender a Abstração é: imagine se você estivesse criando uma máquina para fazer café para seus usuários. Pode haver duas abordagens:

Como criá-la com abstração

  • Ter um botão escrito "Fazer café"

Como criá-la sem abstração

  • Ter um botão escrito "Adicionar água fria à chaleira"
  • Ter um botão escrito "Ferver a água"
  • Ter um botão escrito "Adicionar uma cápsula de café"
  • Ter um botão escrito "Passar a água pela cápsula de café"
  • Além de vários outros botões para completar o processo

É um exemplo muito simples, mas a primeira abordagem abstrai toda a lógica da máquina. A segunda abordagem força o usuário a entender como fazer café e essencialmente fazer o seu próprio.

O próximo pilar é o de Encapsulamento. Mostrarei uma forma de aplicarmos a Abstração, usando-o.

Encapsulamento na Programação Orientada a Objetos

A definição de encapsulamento é "a ação de colocar algo dentro ou como se estivesse em uma cápsula". Remover o acesso a partes do seu código e tornar as coisas privadas é exatamente o que o Encapsulamento faz (muitas vezes, as pessoas se referem a ele como "ocultação de dados").

Encapsulamento significa que o código de cada objeto deve controlar apenas seu próprio estado.  Se você não sabe o que é o estado de um objeto, vamos fazer a seguinte analogia:

Sabe aquele retrato de família, em que você era bebê ainda? Ele é um registro do estado "instantâneo" em que você estava naquele exato momento. De lá pra cá muita coisa mudou, e se hoje você tirar uma nova foto, seu estado já não é o mesmo que aquele. Aquilo que você fez durante o tempo com sua vida, transformou você. A mesma coisa ocorre com o objeto.

O estado é o "instantâneo" atual do objeto. Todas as chaves e métodos (funções) de um objeto são suas propriedades. Se você redefinir ou excluir uma chave, por exemplo, estará alterando o seu estado.

Por isso, é importante limitar o acesso de quais partes do código podem ser acessadas. Caso não sejam necessárias, torne as coisas mais inacessíveis para não possibilitar efeitos colaterais no estado do objeto.

Propriedades privadas são obtidas em JavaScript usando closures. Segue um exemplo abaixo:

var Cachorro = (function () {

	// Privado
	var executar = function () {
		// Implementação de executar
	};
    
	// Privado
	var raca = "Dálmata"
    
	// Público
	var nome = "Rex";

	// Público
	var fazerBarulho = function () {
 		return 'Au au!';
	};

 	return {
		fazerBarulho: fazerBarulho,
		nome: nome
 	};
})();

A primeira coisa que fizemos foi criar uma função que é chamada imediatamente (Immediately Invoked Function Expression, ou de forma abreviada, IIFE). Ela criou um objeto que qualquer um pode acessar, mas escondeu alguns detalhes dentro dela. Você não pode chamar o método executar e nem a chave raca, pois não o expomos no objeto final com o retorno.

Esse padrão específico acima é chamado de Revealing Module Pattern (Padrão do Módulo de Revelação), mas é apenas um exemplo de como você pode obter o encapsulamento.

Vamos, no entanto, focar mais na ideia de Encapsulamento (já que agora é mais importante entender o Encapsulamento como um todo, do que apenas aprender um padrão dele).

Concentre-se mais em como você pode tornar seus dados e código privados e separá-los. Modularizar e ter responsabilidades claras é a chave para a Orientação a Objetos.

Por que devemos preferir a privacidade? Por que não ter tudo acessível globalmente?

  • Muitos bits de código não relacionados se tornarão dependentes/acoplados uns dos outros por meio de variáveis globais.
  • Você provavelmente substituirá as variáveis ​​se o nome delas forem reutilizados, o que pode levar a erros ou comportamentos imprevisíveis.
  • Você provavelmente terminará com um código espaguete – código que é difícil de raciocinar e entender o que está lendo e gravando suas variáveis​, ou onde muda o estado de cada uma.

O encapsulamento pode ser aplicado separando longas linhas de código em funções menores e separadas. É recomendado também separar essas funções em módulos. O objetivo é sempre escondermos os dados em um lugar em que nada mais precise de acesso e expormos os dados de modo claro onde for necessário.

O encapsulamento metaforicamente seria uma casca de noz. Vinculando seus dados a algo, seja uma classe, objeto, módulo ou função, e fazendo o possível para mantê-lo o mais privado possível.

Herança na Programação Orientada a Objetos

A herança permite que um objeto adquira as propriedades e métodos de outro objeto. Em JavaScript, isso é feito por Prototypal Inheritance (ou herança prototípica, em português).

A reutilização é o principal benefício aqui. Sabemos que às vezes a mesma coisa precisa ser feita em vários lugares e sempre de forma igual, exceto em alguma pequena parte. Esse é um problema que a herança pode resolver.

Sempre que usamos herança, tentamos fazer com que o pai e o filho tenham alta coesão. Coesão é o quanto seu código está relacionado. Por exemplo, o tipo Passaro consegue ser estendido do tipo MotorADiesel? Não, certo?

Por isso, mantenha sua herança simples de entender e previsível. Não faça heranças completamente não relacionadas somente porque há um método ou uma propriedade de que você precisa. A herança não resolve bem esse problema específico.

Ao usar herança, ela precisa ter a maior parte das funcionalidades (você nem sempre precisa ter absolutamente tudo).

Nós, desenvolvedores, temos um princípio chamado de princípio de substituição de Liskov. Ele afirma que, se conseguimos usar uma classe pai (vamos chamá-la de TipoPai) em qualquer lugar em que usamos uma classe filho (vamos chamá-la de TipoFilho) – e se TipoFilho é uma herança de TipoPai, então passamos no teste.

A principal razão pela qual se falha nesse teste é se TipoFilho estiver removendo coisas do pai. Se TipoFilho remove métodos herdados do pai, isso gera diversos TypeError, onde haverá coisas que estarão indefinidas e que estamos esperando que não sejam.

fluxograma-1
As flechas parecem estar indo na direção errada. Mas o Animal é a base - o pai.

A "cadeia de herança" é o termo usado para descrever esse fluxo de herança do protótipo do objeto base (aquele do qual todos os outros herdam) até o "final" da cadeia de herança (o último tipo que está herdando – Cachorro, no exemplo acima).

Faça o seu melhor para manter suas cadeias de herança limpas e sensatas. Você pode facilmente acabar programando um antipadrão (antiPattern) ao usar Herança (chamado de antipadrão de base frágil). Isso acontece quando seus protótipos base são considerados "frágeis" – aqueles em que, se você faz uma alteração "segura" no objeto base, todos os seus filhos começam a quebrar.

Polimorfismo na Programação Orientada a Objetos

Polimorfismo significa "a condição de ocorrer de várias formas diferentes". É exatamente com isso que o quarto e último pilar está preocupado – que tipos nas mesmas cadeias de herança sejam capazes de fazer coisas diferentes.

Se você usou a herança corretamente, agora pode usar tanto os pais de maneira confiável como seus filhos. Quando dois tipos compartilham uma cadeia de herança, eles podem ser usados ​​alternadamente sem erros ou declarações em seu código.

A partir do último diagrama, podemos ter um protótipo base chamado Animal que define fazerBarulho. Em seguida, cada tipo que se estende desse protótipo pode substituí-lo para fazer seu próprio trabalho personalizado. Algo assim:

// Vamos configurar um exemplo de Animal e Cachorro
function Animal(){}
function Cachorro(){}

Animal.prototype.fazerBarulho = function(){
	console.log("Barulho base");
};

// A maioria dos animais que programamos tem 4 pernas. Isso pode ser substituído se necessário
Animal.prototype.pernas = 4;

Cachorro.prototype = new Animal();

Cachorro.prototype.fazerBarulho = function(){	console.log("Au au");  
};

var animal = new Animal();
var cachorro = new Cachorro();

animal.fazerBarulho();     // Barulho base
cachorro.fazerBarulho();   // Au au - isso foi substituído
cachorro.pernas;           // 4! Isso foi herdado

Aqui, a classe Cachorro estende Animal e pode fazer uso da propriedade padrão pernas. Porém, ela também é capaz de fazer sua própria implementação de fazer seu próprio barulho.

O verdadeiro poder do polimorfismo é poder compartilhar comportamentos e permitir substituições personalizadas.

Conclusão

Espero que isso tenha explicado quais são os quatro pilares da programação orientada a objetos e como eles levam a um código mais limpo e robusto.

Se você gostou desse artigo e quiser ver mais, eu compartilho meus textos no Twitter.