Encapsulamento, herança, polimorfismo e abstração
Parabéns por chegar à última aula do módulo 6! Nesta etapa final, você se tornará um mestre nos pilares da Programação Orientada a Objetos (POO): encapsulamento, herança, polimorfismo e abstração.
Explore técnicas avançadas para construir classes robustas, reutilizáveis e flexíveis, elevando seu nível de proficiência como desenvolvedor.
Encapsulamento
O encapsulamento é um conceito fundamental na programação orientada a objetos que visa proteger e controlar o acesso aos dados e comportamentos de um objeto.
Podemos comparar o encapsulamento com um cofre: você tem uma caixa que guarda objetos valiosos e apenas você possui a chave para abrir essa caixa.
Imagine que você tem uma classe chamada “Pessoa” que possui informações como nome, idade e endereço.
Essas informações são privadas e não devem ser acessadas ou modificadas diretamente por outras partes do código.
Para garantir o encapsulamento, utilizamos a ideia de “ocultar” os detalhes internos do objeto e fornecer métodos específicos para acessar e modificar esses dados.
Vamos usar a analogia de uma caixa com uma fechadura. A caixa representa o objeto “Pessoa” e a fechadura representa o encapsulamento.
Somente através dos métodos públicos, que seriam as chaves, é possível interagir com os dados dentro da caixa. Por exemplo, temos um método público chamado “getIdade()” que retorna a idade da pessoa.
Para obter essa informação, você precisa chamar esse método, assim como usar a chave correta para abrir a fechadura e acessar o conteúdo da caixa.
O encapsulamento promove a segurança e a integridade dos objetos, evitando que dados sejam corrompidos ou acessados indevidamente.
Além disso, ele permite que você altere a implementação interna do objeto sem afetar outras partes do código, desde que os métodos públicos mantenham a mesma interface. Isso facilita a manutenção e a evolução do seu programa.
Vamos usar o exemplo de uma classe chamada “ContaBancária” que possui os atributos privados “saldo” e “titular“.
O encapsulamento nos permite controlar o acesso a esses atributos através de métodos públicos.
class ContaBancária { constructor(titular, saldoInicial) { this.titular = titular; this.saldo = saldoInicial; } depositar(valor) { this.saldo += valor; } sacar(valor) { if (valor <= this.saldo) { this.saldo -= valor; } else { console.log("Saldo insuficiente"); } } consultarSaldo() { return this.saldo; } }
Nesse exemplo, a classe “ContaBancária” encapsula os dados de uma conta bancária, como o nome do titular e o saldo.
Os atributos “titular” e “saldo” são privados, o que significa que não podem ser acessados diretamente fora da classe. Em vez disso, utilizamos os métodos públicos “depositar“, “sacar” e “consultarSaldo” para interagir com esses atributos.
Por exemplo, ao criar uma instância da classe:
const minhaConta = new ContaBancária("João", 1000);
Podemos depositar dinheiro na conta chamando o método “depositar“:
minhaConta.depositar(500);
Também podemos sacar dinheiro da conta chamando o método “sacar“:
minhaConta.sacar(200);
E para consultar o saldo atual, utilizamos o método “consultarSaldo“:
console.log(minhaConta.consultarSaldo()); // Saída: 1300
Nesse exemplo, o encapsulamento garante que os atributos “titular” e “saldo” sejam protegidos e só possam ser acessados ou modificados através dos métodos públicos da classe.
Isso ajuda a manter a integridade dos dados e evita que eles sejam alterados de forma indevida.
Em resumo, o encapsulamento é como um cofre que protege os dados de um objeto, permitindo o acesso e a modificação apenas através de métodos específicos.
Essa abordagem garante a segurança, a integridade e a flexibilidade do código.
Herança
Para explicar o conceito de herança, vamos utilizar uma analogia com uma família.
Imagine que temos a classe “Pessoa” como a classe base, que possui atributos como nome e idade, e métodos como falar e andar.
Agora, podemos criar classes derivadas, como “Aluno” e “Professor“, que herdam os atributos e métodos da classe “Pessoa”.
Essa relação de herança é semelhante à relação de parentesco em uma família.
Por exemplo, podemos dizer que um “Aluno” é uma “Pessoa” que também possui atributos específicos, como matrícula e instituição de ensino.
Da mesma forma, um “Professor” é uma “Pessoa” com atributos adicionais, como disciplina e salário.
Ao utilizar a herança, não precisamos repetir o código comum entre as classes.
Em vez disso, podemos reutilizar o código da classe base, economizando tempo e esforço.
Além disso, é possível adicionar novos atributos e métodos nas classes derivadas, personalizando-as conforme necessário.
No código JavaScript, a herança é implementada usando a palavra-chave “extends“. Por exemplo, a classe “Aluno” pode ser declarada da seguinte forma:
class Aluno extends Pessoa { constructor(nome, idade, matricula, instituicao) { super(nome, idade); this.matricula = matricula; this.instituicao = instituicao; } estudar() { console.log(`${this.nome} está estudando.`); } }
Nesse exemplo, a classe “Aluno” herda os atributos e métodos da classe “Pessoa” usando a palavra-chave “extends“.
O método “constructor” da classe derivada chama o método “constructor” da classe base usando o “super“, passando os parâmetros correspondentes. Além disso, a classe “Aluno” possui seu próprio método “estudar“.
Dessa forma, ao criar uma instância da classe “Aluno“, teremos acesso tanto aos métodos e atributos da classe “Aluno” quanto aos da classe “Pessoa“. Por exemplo:
const aluno = new Aluno("João", 18, "123456", "Universidade X"); aluno.falar(); // Chama o método "falar" da classe "Pessoa" aluno.estudar(); // Chama o método "estudar" da classe "Aluno"
Assim, o conceito de herança permite que classes derivadas compartilhem características e comportamentos de uma classe base, facilitando a organização e reutilização de código.
Polimorfismo
O polimorfismo é um conceito importante na programação orientada a objetos que nos permite tratar diferentes objetos de forma uniforme, independentemente de sua classe específica.
Para entender o polimorfismo, vamos usar uma analogia com animais.
Imagine que temos várias classes de animais, como “Cachorro“, “Gato” e “Pássaro“, todas derivadas da classe base “Animal“. Cada uma dessas classes possui seu próprio método “fazerSom()“.
Agora, podemos criar uma função chamada “emitirSomDoAnimal” que recebe um objeto animal e chama o método “fazerSom()“.
O polimorfismo entra em ação quando podemos passar diferentes objetos animais para a função “emitirSomDoAnimal” e ela irá chamar o método “fazerSom()” correspondente de cada objeto, independentemente de sua classe específica.
Por exemplo, se passarmos um objeto “Cachorro”, ele latirá; se passarmos um objeto “Gato“, ele miará; e se passarmos um objeto “Pássaro“, ele cantará.
Essa capacidade de tratar objetos diferentes como se fossem do mesmo tipo é o polimorfismo.
Ele nos permite escrever código mais genérico e flexível, pois podemos tratar os objetos de forma uniforme, sem se preocupar com suas classes específicas. Isso facilita a reutilização de código e a manutenção do sistema.
No JavaScript, o polimorfismo é alcançado por meio da herança e da sobrescrita de métodos.
Cada classe derivada pode implementar seu próprio comportamento para o método herdado da classe base, permitindo que objetos de diferentes classes respondam de forma única a uma mesma chamada de método.
Vamos continuar com a analogia dos animais para exemplificar o polimorfismo.
class Animal { fazerSom() { console.log("O animal faz algum som."); } } class Cachorro extends Animal { fazerSom() { console.log("O cachorro faz 'Au au!'"); } } class Gato extends Animal { fazerSom() { console.log("O gato faz 'Miau!'"); } } class Passaro extends Animal { fazerSom() { console.log("O pássaro faz 'Cantos melodiosos!'"); } } function emitirSomDoAnimal(animal) { animal.fazerSom(); } const cachorro = new Cachorro(); const gato = new Gato(); const passaro = new Passaro(); emitirSomDoAnimal(cachorro); // Output: O cachorro faz 'Au au!' emitirSomDoAnimal(gato); // Output: O gato faz 'Miau!' emitirSomDoAnimal(passaro); // Output: O pássaro faz 'Cantos melodiosos!'
Neste exemplo, temos a classe base Animal e três classes derivadas: Cachorro, Gato e Passaro. Cada uma delas sobrescreve o método fazerSom() da classe base com seu próprio comportamento.
A função emitirSomDoAnimal(animal) recebe um objeto animal como parâmetro e chama o método fazerSom() desse objeto.
Dependendo do tipo de animal passado, o método correspondente será executado, resultando em diferentes sons emitidos.
Assim, mesmo tratando todos os objetos como do tipo Animal, o polimorfismo nos permite chamar o método específico de cada classe derivada.
Isso é possível porque todas as classes compartilham uma relação de herança e cada uma tem sua própria implementação do método.
Esse exemplo ilustra como o polimorfismo nos permite tratar objetos de diferentes classes de forma uniforme, proporcionando maior flexibilidade e reutilização de código.
Em resumo, o polimorfismo nos permite tratar objetos de diferentes classes como se fossem do mesmo tipo, proporcionando uma maior flexibilidade e reutilização de código em nossos programas.
Ele é um dos pilares da programação orientada a objetos e ajuda a criar sistemas mais escaláveis e eficientes.
Abstração
Vou explicar o conceito de abstração usando uma analogia com um controle remoto de TV.
Imagine que você tem um controle remoto com vários botões e cada um deles tem uma função específica, como ligar/desligar, trocar de canal e ajustar o volume.
Quando você usa o controle remoto, não precisa entender como ele funciona internamente, quais são os componentes eletrônicos ou o processo exato para enviar um sinal para a TV.
Tudo o que você precisa saber são as funções disponíveis e como usá-las.
A abstração na programação orientada a objetos segue a mesma ideia. Ela nos permite representar objetos do mundo real ou conceitos complexos por meio de uma interface simples e de fácil entendimento.
Essa interface define quais são as operações disponíveis e como utilizá-las, ocultando os detalhes internos de implementação.
Em termos de programação, a abstração nos permite criar classes e objetos que encapsulam os detalhes internos e expõem apenas os métodos e propriedades relevantes para o uso externo.
Isso facilita o desenvolvimento, pois os programadores podem interagir com os objetos sem precisar conhecer todos os detalhes de sua implementação.
A analogia do controle remoto ilustra como podemos interagir com objetos por meio de uma interface abstrata, usando apenas os métodos e propriedades fornecidos, sem se preocupar com o funcionamento interno do objeto.
Assim, a abstração nos permite simplificar a complexidade, criar objetos mais legíveis e reutilizáveis, além de promover a modularidade e o encapsulamento do código.
Vamos ver em um exemplo:
abstract class Conta { constructor(saldo) { this.saldo = saldo; } depositar(valor) { this.saldo += valor; } abstract sacar(valor); // Método abstrato que deve ser implementado nas classes filhas } class ContaCorrente extends Conta { constructor(saldo, limite) { super(saldo); this.limite = limite; } sacar(valor) { if (valor <= this.saldo + this.limite) { this.saldo -= valor; } else { console.error("Saldo insuficiente"); } } } class ContaPoupanca extends Conta { constructor(saldo) { super(saldo); } sacar(valor) { if (valor <= this.saldo) { this.saldo -= valor; } else { console.error("Saldo insuficiente"); } } } var contaCorrente = new ContaCorrente(1000, 500); var contaPoupanca = new ContaPoupanca(500); contaCorrente.sacar(800); // Sucesso contaPoupanca.sacar(600); // Sucesso contaCorrente.sacar(1600); // Saldo insuficiente // A classe Conta é abstrata e não pode ser instanciada diretamente, // pois o método abstrato "sacar" precisa ser implementado nas classes filhas.
É um conceito fundamental na programação orientada a objetos que nos ajuda a lidar com a complexidade de sistemas de software.
Desafio:
- Crie uma classe abstrata
Veiculo
com as propriedadesmodelo
emarca
. - Defina um método abstrato
acelerar
que deve ser implementado nas classes filhas. - Crie classes filhas
Carro
eMotocicleta
que herdam deVeiculo
e implementam o métodoacelerar
de forma específica. - Crie objetos de cada classe e utilize o método
acelerar
.
Parabéns por concluir o módulo 6! Agora você possui uma base sólida para aplicar os conceitos da POO em seus projetos de desenvolvimento JavaScript, criando programas mais organizados, eficientes e reutilizáveis
Agora é hora de colocar seus conhecimentos em prática com um exercício do módulo.