Published on

Entendendo Programação Orientada a Objetos (POO) com Exemplos em TypeScript

Authors
  • avatar
    Name
    Alysson Rodrigues
    Twitter
    @066aly

1. Classes e Objetos

Uma classe é um modelo para criar objetos. Ela define propriedades e métodos que os objetos criados a partir da classe terão. Um objeto é uma instância de uma classe.

  • Exemplo: A classe Carro define um modelo para carros com uma propriedade modelo e um método ligarMotor. meuCarro é um objeto (uma instância) criado a partir da classe Carro, especificamente um modelo 'Sedan'.
// vehicle.ts
// Classe representando um Carro
class Carro {
  // Propriedade privada para armazenar o modelo do carro
  private modelo: string;

  // Construtor para inicializar o modelo do carro
  constructor(modelo: string) {
    this.modelo = modelo;
  }

  // Método público para simular a partida do motor do carro
  public ligarMotor(): void {
    console.log('Motor ligado');
    // Registra uma mensagem indicando que o motor foi ligado
  }
}

// Cria uma instância da classe Carro com o modelo 'Sedan'
const meuCarro = new Carro('Sedan');

// Registra a instância do Carro no console
console.log(meuCarro); // Saída: Carro { modelo: 'Sedan' }

2. Encapsulamento

Encapsulamento é o agrupamento de dados (propriedades) e métodos que operam sobre esses dados dentro de uma única unidade (uma classe). Também envolve restringir o acesso direto a alguns dos componentes de um objeto, o que é um aspecto chave do ocultamento de dados (data hiding). Modificadores de acesso como public, private e protected controlam essa visibilidade.

  • private: Membros são acessíveis apenas de dentro da classe que os define. Exemplo: Em ContaBancaria, numeroConta e obterNumeroConta são privados. Você não pode acessar conta.numeroConta diretamente de fora da classe. O acesso é controlado através de métodos públicos como exibirNumeroContaParcial. Similarmente, modero em Carro é privado.
  • public: Membros são acessíveis de qualquer lugar (este é o padrão se nenhum modificador for especificado). Exemplo: exibirNumeroContaParcial em ContaBancaria, nome e fazerSom em Animal, raça e latir em Cachorro, ligarMotor em Carro, e dirigir em Carro são públicos.
  • protected: Membros são acessíveis dentro da classe que os define e por instâncias de subclasses (classes derivadas). Exemplo: Em Veículo (A primeira imagem abaixo), nivelCombustivel e verificarCombustivel são protegidos. A classe Carro, que estende Vehicle, pode acessar verificarCombustivel diretamente dentro de seu método dirigir, mas você não poderia chamar theCar.checkFuel() de fora da classe CarClass ou Vehicle.
// vehicle.ts
// Classe base representando um Veículo genérico
class Veiculo {
  // Propriedade protegida para armazenar o nível de combustível do veículo
  protected nivelCombustivel: number;

  // Construtor para inicializar o nível de combustível do veículo
  constructor(nivelCombustivel: number) {
    this.nivelCombustivel = nivelCombustivel;
  }

  // Método protegido para verificar se o nível de combustível é suficiente
  // Retorna verdadeiro se o nível de combustível for maior que 10
  protected verificarCombustivel(): boolean {
    return this.nivelCombustivel > 10;
  }
}

// Classe derivada representando um tipo específico de Veículo: Carro
// Nota: Renomeei 'CarClass' para 'Carro' para seguir o padrão do exemplo anterior
// e para ser mais idiomático em português.
class Carro extends Veiculo {
  // Método público para simular a condução do carro
  public dirigir(): void {
    // Verifica se o nível de combustível é suficiente usando o método herdado verificarCombustivel
    if (this.verificarCombustivel()) {
      console.log('Dirigindo...'); // Saída se o nível de combustível for suficiente
    } else {
      console.log('Combustível baixo!'); // Saída se o nível de combustível for insuficiente
    }
  }
}

// Cria uma instância da classe Carro com um nível inicial de combustível de 11
const meuCarro = new Carro(11); // Usei 'meuCarro' como no exemplo anterior

// Chama o método dirigir na instância do Carro
// Saída: 'Dirigindo...' porque o nível de combustível (11) é maior que 10
meuCarro.dirigir();
// Classe representando uma Conta Bancária
class ContaBancaria {
  // Propriedade privada para armazenar o número da conta
  // Nota: É convenção usar 'string' (tipo primitivo) em vez de 'String' (objeto) em TypeScript.
  private numeroConta: string;

  // Construtor para inicializar o número da conta
  constructor(numeroConta: string) {
    this.numeroConta = numeroConta;
  }

  // Método privado para obter o número completo da conta
  // Este método não é acessível fora da classe
  private obterNumeroConta(): string {
    return this.numeroConta;
  }

  // Método público para exibir o número da conta parcialmente mascarado
  public exibirNumeroContaParcial(): void {
    // Registra o número da conta com os primeiros 4 dígitos mascarados
    // Usando 'this.numeroConta' que agora está em português
    console.log('Número da Conta: ****' + this.numeroConta.slice(4));
  }
}

// Cria uma instância da classe ContaBancaria com um número de conta específico
const conta = new ContaBancaria('456049584456');

// Chama o método exibirNumeroContaParcial para mostrar o número da conta mascarado
// Saída: 'Número da Conta: ****9584456'
conta.exibirNumeroContaParcial();

3. Herança

Herança permite que uma classe (subclasse ou classe derivada) herde propriedades e métodos de outra classe (superclasse ou classe base). Isso promove a reutilização de código. A palavra-chave extends é usada para estabelecer a herança.

  • Exemplo 1: A classe Cachorro estende a classe Animal. Cachorro herda a propriedade nome e o método fazerSom de Animal. Ela também adiciona sua própria propriedade (raça) e método (latir), e sobrescreve o método fazerSom. A chamada super(nome) no construtor de Cachorro invoca o construtor de Animal.
  • Exemplo 2: A Carro (no exemplo acima) estende Veiculo, herdando nivelCombustivel e verificarCombustivel.
  • Exemplo 3: Circulo e Retangulo ambos estendem Forma, herdando o método base calcularArea (embora eles o sobrescrevam).
// Classe base representando um Animal genérico
class Animal {
  // Propriedade pública para armazenar o nome do animal
  public nome: string; // Usando 'string'

  // Construtor para inicializar o nome do animal
  constructor(nome: string) {
    this.nome = nome;
  }

  // Método para simular o animal fazendo um som genérico
  fazerSom(): void { // Adicionado tipo de retorno 'void' para clareza
    console.log('Fazendo algum som genérico...');
  }
}

// Classe derivada representando um tipo específico de Animal: Cachorro
class Cachorro extends Animal {
  // Propriedade pública para armazenar a raça do cachorro
  public raca: string; // Usando 'string'

  // Construtor para inicializar o nome e a raça do cachorro
  // Chama o construtor da classe pai (Animal) para definir o nome
  constructor(nome: string, raca: string) {
    super(nome); // Chama o construtor da classe pai (Animal)
    this.raca = raca;
  }

  // Método sobrescrito (override) para simular o cachorro fazendo um som específico
  public fazerSom(): void { // Sobrescrevendo o método da classe pai
    console.log('Au au!'); // Som mais específico para cachorro
  }

  // Método específico da classe Cachorro para simular o latido
  latir(): void {
    console.log('Latindo...');
  }
}

// Cria uma instância da classe Cachorro com nome e raça
const cachorro = new Cachorro('Pimpolho', 'Salsicha');

// Chama o método latir na instância do Cachorro
// Saída: 'Latindo...'
cachorro.latir();

// Exemplo adicional: chamando o método sobrescrito
// Saída: 'Au au!'
// cachorro.fazerSom();

4. Polimorfismo

Polimorfismo ("muitas formas") permite que objetos de diferentes classes sejam tratados como objetos de uma superclasse ou interface comum. Frequentemente se manifesta de duas maneiras: Sobrescrita de Método (Method Overriding) e Interfaces.

  • Sobrescrita de Método: Uma subclasse fornece uma implementação específica para um método que já está definido em sua superclasse. Exemplo 1: Cachorro sobrescreve o método fazerSom herdado de Animal para fornecer um som específico de cachorro ('Au au!!') em vez do genérico. Exemplo 2: Circulo e Retangulo ambos sobrescrevem o método calcularArea de Forma para realizar cálculos específicos para sua geometria. A função imprimirArea recebe qualquer objeto Shape, mas chama o método calcularArea sobrescrito correto com base no tipo real do objeto (Circulo ou Retangulo) passado para ela.
  • Interfaces (Polimorfismo Implícito via Duck Typing/Tipagem Estrutural em TypeScript): Uma interface define um contrato para uma certa estrutura ou comportamento. Classes podem implementar interfaces, garantindo que forneçam métodos específicos. Funções podem então operar sobre qualquer objeto que cumpra o contrato da interface, independentemente de sua classe específica.
/**
 * Classe base representando uma forma geométrica.
 */
class Forma {
  /**
   * Calcula a área de uma forma.
   * @returns A área calculada como um número. Retorna 0 na classe base,
   * pois uma forma genérica não tem área definida.
   * Classes derivadas devem sobrescrever este método.
   */
  public calcularArea(): number {
    return 0;
  }
}

/**
 * Representa uma forma de círculo, estendendo a classe base `Forma`.
 * Fornece funcionalidade para calcular a área do círculo.
 */
class Circulo extends Forma {
  // O construtor declara e inicializa a propriedade pública 'raio' diretamente.
  constructor(public raio: number) {
    super(); // Chama o construtor da classe pai (Forma)
  }

  /**
   * Calcula a área do círculo (π * raio²).
   * @returns A área calculada como um número.
   */
  public calcularArea(): number {
    return Math.PI * this.raio * this.raio;
    // Ou: return Math.PI * Math.pow(this.raio, 2);
  }
}

/**
 * Representa uma forma de retângulo, estendendo a classe base `Forma`.
 * Fornece funcionalidade para calcular a área do retângulo.
 */
class Retangulo extends Forma {
  // O construtor declara e inicializa as propriedades públicas 'largura' e 'altura'.
  constructor(public largura: number, public altura: number) {
    super(); // Chama o construtor da classe pai (Forma)
  }

  /**
   * Calcula a área do retângulo (altura * largura).
   * @returns A área calculada como um número.
   */
  public calcularArea(): number {
    return this.altura * this.largura;
  }
}

/**
 * Registra (imprime) a área de uma forma geométrica dada no console.
 * @param forma - Uma instância de uma classe que estende a classe base `Forma` (ex: Circulo, Retangulo).
 * Graças ao polimorfismo, o método `calcularArea` correto será chamado.
 */
function imprimirArea(forma: Forma): void { // Especifica o tipo de retorno void
  // Chama o método calcularArea do objeto específico (Circulo ou Retangulo)
  console.log("Área:", forma.calcularArea());
}

// Cria instâncias das formas específicas
const circulo = new Circulo(5); // Um círculo com raio 5
const retangulo = new Retangulo(10, 4); // Um retângulo 10x4

// Imprime a área de cada forma usando a mesma função
imprimirArea(circulo);    // Saída esperada: Área: 78.5398...
imprimirArea(retangulo);  // Saída esperada: Área: 40

Estes exemplos demonstram como os princípios da POO ajudam a organizar o código em unidades reutilizáveis, gerenciáveis e extensíveis usando classes, encapsulamento, herança e polimorfismo em TypeScript.

Usei isso para aprender POO a fim de passar no meu exame da faculdade. Se você quiser se aprofundar no entendimento, do começo ao fim, estes guias abrangentes podem te ajudar a começar.

Estou meio nervoso com essa coisa de blog, então deixe seu feedback abaixo, seria muito apreciado!