A orientação a objeto é um paradigma essencial na programação moderna. JavaScript, apesar de sua flexibilidade como linguagem de múltiplos paradigmas, permite uma abordagem robusta nesse modelo. Assim como em outras linguagens, a orientação a objetos no JavaScript envolve conceitos fundamentais como classes, objetos, encapsulamento, herança e polimorfismo.
Trago um exemplo no GitHub do jogo Banco Imobiliário, para isso vamos analisar a Classe Conta.
Classe de um Objeto
Objetos são a base desse paradigma. Em JavaScript, eles podem ser criados de maneira literal ou através de funções construtoras. No entanto, com a introdução das classes na ECMAScript 6 (ES6), a organização do código tornou-se mais estruturada e intuitiva. A palavra-chave “class” permite definir um modelo para criação de objetos, facilitando a reutilização de código e promovendo a legibilidade.
A classe “Conta” exemplifica bem esses princípios. Ela encapsula propriedades como “nome”, “salario”, “saldoInicial” e “lista_imoveis”, protegendo os dados e garantindo que a lógica seja aplicada corretamente. Além disso, métodos como “pegarEmprestimo” e “atualizarSaldo” permitem interações específicas, garantindo que as operações financeiras sejam executadas de maneira controlada.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
class Conta { constructor(nome, salario, saldoInicial = 0) { this.nome = nome; this.saldo = saldoInicial || salario; this.salario = salario; this.lista_imoveis = {}; this.emprestimo = { valor: 0, parcelas_a_pagar: [] }; this.numeroCartao = Math.floor(Math.random() * 90) + 10; this.falido = false; } pegarEmprestimo(valor, parcelas, banco) { const valorMaximoEmprestimo = 0.1 * this.calcularValorTotalImoveis(); if (valor > valorMaximoEmprestimo) { this.log(`Valor máximo de empréstimo é ${valorMaximoEmprestimo} (10% dos Imóveis).`); return; } const valorParcela = (valor * 1.1) / parcelas; this.emprestimo.valor = valor; this.emprestimo.parcelas_a_pagar = []; // Garante que a lista está vazia antes de adicionar novas parcelas. for (let i = 0; i < parcelas; i++) { this.emprestimo.parcelas_a_pagar.push(valorParcela); } this.saldo += valor; this.log(`${this.nome} pegou um empréstimo de ${valor} em ${parcelas} parcelas.`); } calcularValorTotalImoveis() { return Object.values(this.lista_imoveis).reduce((acc, val) => acc + val, 0); } atualizarSaldo(banco) { this.saldo += this.salario; if (this.emprestimo.parcelas_a_pagar.length > 0) { const valorParcela = this.emprestimo.parcelas_a_pagar.shift(); this.saldo -= valorParcela; banco.saldo += valorParcela - (valorParcela / 1.1); this.log(`Parcela de ${valorParcela} abatida do empréstimo de ${this.nome}.`); // Se não houver mais parcelas, o empréstimo é quitado if (this.emprestimo.parcelas_a_pagar.length === 0) { this.emprestimo.valor = 0; this.log(`${this.nome} quitou seu empréstimo.`); } } // Se o saldo ficou negativo, liquidar imóveis automaticamente while (this.saldo < 0 && Object.keys(this.lista_imoveis).length > 0) { let imovelMaisCaro = Object.entries(this.lista_imoveis).sort((a, b) => b[1] - a[1])[0]; if (imovelMaisCaro) { let [imovelNome, valor] = imovelMaisCaro; delete this.lista_imoveis[imovelNome]; this.saldo += valor; banco.lista_imoveis[imovelNome] = valor; // Transfere imóvel ao banco this.log(`Imóvel ${imovelNome} vendido por ${valor} para cobrir dívida.`); } } // Se ainda estiver negativo após vender tudo, está falido if (this.saldo < 0) { this.falido = true; this.log(`${this.nome} está falido e fora do jogo.`); } this.log(`${this.nome} atualizou o saldo para ${this.saldo}.`); } liquidarDividas() { const propriedades = Object.entries(this.lista_imoveis).sort((a, b) => b[1] - a[1]); for (let [nome, valor] of propriedades) { if (this.saldo >= 0) break; delete this.lista_imoveis[nome]; this.saldo += valor; this.log(`${nome} será leiloado.`); } if (this.saldo < 0) { this.log(`${this.nome} está falido.`); this.falido = true; } } extrato() { this.log(`Extrato da conta ${this.nome}:`); this.log(` Saldo: ${this.saldo}`); this.log(` Salário: ${this.salario}`); this.log(` Lista de Imóveis: ${JSON.stringify(this.lista_imoveis)}`); this.log(` Empréstimo: ${this.emprestimo.valor > 0 ? `Valor: ${this.emprestimo.valor}, Parcelas a pagar: ${JSON.stringify(this.emprestimo.parcelas_a_pagar)}` : 'Nenhum'}`); } log(message) { const logDiv = document.getElementById('log'); const p = document.createElement('p'); // Substitui as novas linhas por <br> para exibir corretamente no HTML message = message.replace(/n/g, '<br>'); p.innerHTML = message.replace(/ /g, '&nbsp;'); logDiv.appendChild(p); logDiv.scrollTop = logDiv.scrollHeight; } } |
Analogamente, a classe “Banco” centraliza a gestão das contas e transações. A propriedade “contas” armazena os correntistas, enquanto métodos como “extratoDeTodos” e “transferir” possibilitam uma visão abrangente e a movimentação de valores entre jogadores. Dessa forma, o código promove uma clara separação de responsabilidades, o que facilita a manutenção e a expansão futura.
Encapsulamento
O conceito de encapsulamento significa que certos dados e funcionalidades de um objeto não devem ser diretamente acessados de fora da classe. Embora JavaScript não possua modificadores de acesso como “private” e “protected” nativamente, práticas como a utilização de closures e convenções (como prefixar propriedades privadas com um underline) ajudam a preservar essa ideia. Por exemplo, ao definir propriedades dentro do construtor sem métodos de acesso público, garantimos que apenas a própria classe manipule esses valores.
Além disso, a herança é um dos pilares desse paradigma. Com ela, classes podem estender outras classes, reaproveitando métodos e propriedades sem precisar reescrevê-los. Embora o código apresentado não implemente herança diretamente, seria possível criar subclasses para diferentes tipos de contas, como “ContaCorrente” e “ContaPoupanca”, cada uma com regras específicas de operação.
Polimorfismo
O polimorfismo, por sua vez, permite que diferentes classes compartilhem métodos com comportamentos distintos. Suponha que a classe “Conta” possua um método “extrato”. Se criássemos uma classe “ContaPremium” que sobrescrevesse esse método para fornecer um extrato mais detalhado, estaríamos utilizando polimorfismo de sobrescrita, uma técnica valiosa para modularidade e personalização.
A lógica de um jogo financeiro como o apresentado demonstra a importância desses conceitos. Ao estruturar o código com classes e métodos bem definidos, evita-se redundância e facilita-se a compreensão das regras. Além disso, funcionalidades como “comprarImovel” e “sortearOrdemJogada” evidenciam como métodos encapsulam ações específicas, promovendo a organização do código.
É inegavelmente vantajoso adotar a orientação a objetos em projetos JavaScript. Além de melhorar a organização e a escalabilidade, esse paradigma favorece boas práticas de desenvolvimento, como o princípio da responsabilidade única (SRP), que preconiza que cada classe deve ter apenas um motivo para mudar. Dessa maneira, um código orientado a objetos tende a ser mais sustentável a longo prazo.
Programação OO ou funcional
Vale mencionar que JavaScript permite abordagens híbridas. Ou seja, nem sempre é necessário seguir um modelo puramente orientado a objetos. Em alguns casos, o uso de funções e programação funcional pode ser mais eficiente. Entretanto, para sistemas mais complexos, a orientação a objetos continua sendo uma escolha sólida.
Portanto, ao desenvolver aplicações JavaScript, é fundamental compreender e aplicar os conceitos de orientação a objetos. Um código bem estruturado não apenas melhora a legibilidade, mas também facilita a manutenção e a colaboração em equipe. Dessa forma, ao implementar classes, encapsular dados e reaproveitar funcionalidades, os desenvolvedores podem criar soluções mais robustas e eficientes, promovendo a escalabilidade e a reutilização de código em projetos diversos.