Conteúdo do curso
Apresentação do curso
0/1
Curso de JavaScript para Iniciantes: Fundamentos e Prática
Sobre a Aula

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:

  1. Crie uma classe abstrata Veiculo com as propriedades modelo e marca.
  2. Defina um método abstrato acelerar que deve ser implementado nas classes filhas.
  3. Crie classes filhas Carro e Motocicleta que herdam de Veiculo e implementam o método acelerar de forma específica.
  4. 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.

Entrar na conversa
Rolar para cima