6. POLIMORFISMO AD HOC

“A função do bom software é de fazer o complexo parecer ser simples e a tarefa da equipe de desenvolvimento de software é de projetar a ilusão de simplicidade.”

Grady Booch

O polimorfismo junto com abstração, encapsulamento e herança, formam os famosos 4 pilares da programação orientada a objetos11. A abstração já foi introduzida no Capítulo 02: Modelo de Objetos. Este capítulo é dedicado a introduzir o polimorfismo e trabalhar com seu tipo mais básico, o polimorfismo ad hoc, que é um ótimo candidato para os primeiros passos nessa dinâmica de tipos.

6.1 Conceito de Polimorfismo

O polimorfismo é uma característica dos tipos, que são estudados com mais profundidade nas áreas da matemática, lógica e ciência da computação, em uma área do conhecimento chamada teoria dos tipos.

No entanto, na programação os tipos são estudados mais pragmaticamente, como a utilidade de definir tipos de variáveis, de retorno, de arrays e a criação de novos tipos através de registros (structs) e classes.

Para efeitos práticos, o polimorfismo é entendido como a possibilidade de enviar mensagens iguais a tipos diferentes ou a disponibilidade de uma única interface para diferentes tipos. Por isso, se diz que a interface respeita ou possui muitas formas (do Grego: poli = muitas, morphos = formas).

A área de estudo dos tipos e do polimorfismo é bastante ampla. No entanto, as três classes de polimorfismo comuns e disponíveis nas linguagens de programação são o ad hoc, por subtipagem e paramétrico.

O polimorfismo é dos recursos chave da POO (e da programação em geral). Está amplamente associado ao reaproveitamento de lógica através da variação dos algoritmos. Reaproveitamento é primordial na indústria de software, pois se traduz em menos trabalho, e menos trabalho em menos dinheiro e tempo. Portanto, abstração e polimorfismo estão no centro de um redemoinho tanto técnico, tecnológico como, diria até principalmente, econômico.

6.2 O que é o Polimorfismo ad hoc

O polimorfismo ad hoc se baseia em funções polimórficas que, embora conservem o mesmo nome, recebem diferentes tipos de parâmetros e podem variar de acordo com os argumentos passados.

Ele se diferencia dos outros tipos de polimorfismo, o por subtipagem e parametrização dos tipos, ambos baseados no sistema de tipos. Por esta razão ele é chamado de ad hoc12.

O polimorfismo ad hoc não está disponível em todas as linguagens orientadas a objetos, por não ser um recurso fundamental para o sistema de tipos. Nas linguagens que o implementam, ele é disponibilizado sob o nome de sobrecarga de métodos (ou sobrecarga de funções em linguagens não-OO ou que preferem este termo).

6.3 Sobrecarga de Métodos

A sobrecarga de métodos permite definir múltiplas funções com o mesmo nome e retorno mas com diferentes implementações. O método específico que a ser invocado é definido pelo contexto do algoritmo. Este contexto, por sua vez, é determinando pelos argumentos passados ao método (ou à função).

Portanto, as várias formas (o polimorfismo) assumidas pela função respondem à quantidade ou tipos dos argumentos passados. Por exemplo, considere um sistema imobiliário, onde um método alugar():Aluguel pode ser disponibilizado para que seja passada um valor reserva ou o nome do fiador, como alugar(reserva:numérico):Aluguel ou alugar(fiador:textual):Aluguel. O método específico dependerá do contexto, dado pelos argumentos, isto é, enquanto imovel.alugar(2000) invocará a primeira, imovel.alugar("Marcio") invocará a segunda versão do método alugar.

Considere um exemplo simples, como este e-book onde pode-se movimentar entre as páginas. Veja o pseudocódigo:

// EBook.pseudo
classe EBook
  atributo pagina : inteiro = 1

  método avançar() : vazio
    atributo pagina = atributo pagina + 1
  fim método

  // método avançar sobrecarregado
  método avançar(qtd : inteiro) : vazio
    atributo pagina = atributo pagina + qtd
  fim método

  // método avançar sobrecarregado
  método avançar(título: textual) : vazio
    escolha título
      caso "A história da POO"
        atributo pagina = 10
      caso "Modelo de Objetos"
        atributo pagina = 25
      caso "Estado"
        atributo pagina = 33
    fim escolha
  fim método
fim classe

// App.pseudo
usando EBook de EBook.pseudo
procedimento App
  ebook = novo EBook
  imprime(ebook.pagina) // imprime 1
  // avançar sem argumentos avança uma página apenas
  ebook.avançar()
  imprime(ebook.pagina) // imprime 2
  // avançar com argumento numérico avança "x" páginas
  ebook.avançar(3)
  imprime(ebook.pagina) // imprime 5
  // avançar com argumento textual avança até o título
  ebook.avançar("Modelo de Objetos")
  imprime(ebook.pagina) // imprime 25
fim procedimento

Eu forcei um pouco o exemplo anterior, pois avançar("A história da POO") não seria bem um avançar, mas um retroceder. No entanto, a simplicidade do exemplo está na sobrecarga do método avançar, com opção sem parâmetro avançar(), com parâmetro numérico avançar(numérico) e textual avançar(textual). A abstração do avançar funciona com o movimento das páginas, seja uma ou várias. As várias formas estão codificadas na variação do tipo dos argumentos.

Para um exemplo mais prático e útil vamos resgatar o carrinho, no entanto permitindo a adição de um ou vários produtos e a remoção pelo número e nome do produto. Veja o pseudocódigo:

// Carrinho.pseudo
classe Carrinho
  atributo produtos : lista = []

  método adicionar(produto: texto) : vazio
    atributo produtos.adicionar(produto)
  fim método
  // adicionar sobrecarregado pela quantidade de parâmetros
  método adicionar(produto: texto, quantidade: inteiro) : vazio
    considere i : inteiro = 0
    enquanto i < quantidade
      atributo produtos.adicionar(produto)
      incrementa i
    fim enquanto
  fim método

  método remover(produto : texto) : vazio
    enquanto atributo produtos.contem(produto)
      atributo produtos.remover(produto)
    fim enquanto
  fim método
  // remover sobrecarregado pelo tipo de parâmetro
  método remover(número : inteiro) : vazio
    indice = número - 1
    atributo produtos.remover(indice)
  fim método
fim classe

// App.pseudo
usando Carrinho de Carrinho.pseudo
procedimento App
  carr = novo Carrinho
  // adicionar um produto
  carr.adicionar("Teclado")
  // adicionar vários produtos através do método sobrecarregado
  carr.adicionar("Mouse", 5)
  // remover o segundo produto (um mouse)
  carr.remover(2)
  // remover o teclado
  carr.remover("Teclado")
fim procedimento

Neste exemplo podem ser observadas duas sobrecargas, a do método adicionar e do remover. A primeira foi feita com a variação da quantidade de parâmetros na forma de adicionar(textual) e adicionar(textual, inteiro), e a segunda na variação do tipo como remover(textual) e remover(inteiro). A sobrecarga pode ser obtida tanto variando o tipo do parâmetro quanto a quantidade deles.

Para finalizar este tópico segue a implementação em Java:

// Carrinho.java
import java.util.ArrayList;
class Carrinho {
  ArrayList produtos = new ArrayList();
  void adicionar(String produto) {
    this.produtos.add(produto);
  }
  void adicionar(String produto, int quantidade) {
    for (int i = 0; i < quantidade; i++) {
      this.produtos.add(produto); // ou this.adicionar(produto)
    }
  }
  void remover(String produto) {
    while (this.produtos.contains(produto)) {
      this.produtos.remove(produto);
    }
  }
  void remover(int numero) {
    int indice = numero - 1; // indíce inicia do zero
    // o método remove de ArrayList já é sobrecarregado
    this.produtos.remove(indice);
  }
}
// App.java
class App {
  public static void main(String[] args) {
    Carrinho carr = new Carrinho();
    // adicionar um produto
    carr.adicionar("Teclado");
    System.out.println(carr.produtos.size() == 1);
    // adicionar vários produtos através do método sobrecarregado
    carr.adicionar("Mouse", 5);
    System.out.println(carr.produtos.size() == 6);
    // remover o segundo produto (um mouse)
    carr.remover(2)
    System.out.println(carr.produtos.size() == 5);
    // remover o teclado
    carr.remover("Teclado")
    System.out.println(carr.produtos.size() == 4);
  }
}

6.4 Sobrecarga de Construtores

Assim como os métodos os construtores também podem ser sobrecarregados. Isto é, pode-se ter dois ou mais construtores desde que tenham número e/ou tipo diferentes de parâmetros. Deste modo, é possível instanciar um objeto a partir de informações diferentes de inicialização, porém válidas. Assim como no caso dos métodos, o construtor que será invocado dependerá dos argumentos passados na instrução new.

Por exemplo, considere uma classe Horario que permite armazenar horas e minutos. Considere que embora os atributos sejam números inteiros, deve-se ser possível instanciá-la através de uma string de hora como "09:36". Segue a implementação em Java:

// Horario.java
class Horario {
  // atributos
  int horas;
  int minutos;
  // construtores
  Horario(int horas, int minutos) { // int, int, ex.: 6, 36
    this.horas = horas;
    this.minutos = minutos;
  }
  Horario(String horario) { // String, ex.: "06:36"
    String[] split = horario.split(":"); // "06:36" -> ["06", "36"]
    this.horas = Integer.parseInt(split[0]);
    this.minutos = Integer.parseInt(split[1]);
  }
}
// App.java
class App {
  public static void main(String[] args) {
    // instanciando Horario com o construtor int, int
    Horario horario = new Horario(5, 57);
    System.out.println(horario.horas == 5);
    System.out.println(horario.minutos == 57);

    // instanciando Horario com o construtor String
    horario = new Horario("03:39");
    System.out.println(horario.horas == 3);
    System.out.println(horario.minutos == 39);
  }
}

No exemplo anterior o construtor foi sobrecarregado para aceitar um parâmetro textual, além de dois inteiros. É possível sobrecarregar mais, desde que não sejam repetidos o mesmo número e tipo de parâmetros. Isto é, não pode haver outro construtor que receba dois ints ou uma string.

A sobrecarga de construtores também é um artifício para declarar parâmetros opcionais e valores padrão. Por exemplo, considere que para instanciar Horario os minutos sejam opcionais e new Horario(13) instancia 13:00. A seguir a implementação em Java:

// Horario.java
class Horario {
  // atributos
  int horas;
  int minutos;
  // construtores
  Horario() { // new Horario() -> "00:00"
    this.horas = 0;
    this.minutos = 0;
    // ou
    // this(0, 0)
  }
  Horario(int horas) { // int, ex.: 13
    this.horas = horas;
    this.minutos = 0;
    // ou
    // this(horas, 0)
  }
  Horario(int horas, int minutos) {
    this.horas = horas;
    this.minutos = minutos;
  }
  Horario(String horario) {
    String[] split = horario.split(":");
    this.horas = Integer.parseInt(split[0]);
    this.minutos = Integer.parseInt(split[1]);
  }
}

No exemplo anterior a classe Horario pode ser instanciada de quatro formas diferentes: new Horario(), new Horario(0), new Horario(0, 0) e new Horario("00:00"). Os três primeiros construtores são usados para flexibilizar a quantidade de argumentos necessários, permitindo instanciar 13:00 como new Horario(13) em vez de new Horario(13, 0), assumindo o valor default 0 quando os argumentos não são passados.

6.5 Considerações

Como já foi dito, o polimorfismo é uma funcionalidade chave da POO. Portanto, dominar este recurso é essencial para tornar-se um desenvolvedor de sistemas orientados a objetos. Por outro lado, ele também é muitas vezes mal entendido ou usado. Considere que ao sobrecarregar métodos e construtores se está adicionando novos pontos de mudança no seu código, então use este recurso com sabedoria. A quantidade de bugs é proporcional a quantidade de código, logo, menos código == menos bugs.

6.6 Exercícios

Seguem alguns exercícios onde o polimorfismo ad hoc pode ser usado.

Passagem rodoviária

Considere a compra de passagens na modalidade rodoviária. Para simplificação, considere uma Viagem em um ônibus com 25 assentos. Portanto, ao comprar uma passagem o cliente pode escolher o assento, senão será escolhido o primeiro livre. Implemente a classe Viagem e o comportamento esperado segundo os casos de teste a seguir:

// App.java
class App {
  public static void main(String[] args) {
    Viagem rg_poa = new Viagem("Rio Grande", "Porto Alegre");
    // 25 assentos
    System.out.println(rg_poa.quantidadeAssentosDisponiveis() == 25);
    // numerados de 1 a 25
    System.out.println(rg_poa.estaDisponivel(1) == true);
    System.out.println(rg_poa.estaDisponivel(2) == true);
    System.out.println(rg_poa.estaDisponivel(25) == true);
    // inválidos
    System.out.println(rg_poa.estaDisponivel(0) == false);
    System.out.println(rg_poa.estaDisponivel(26) == false);
    System.out.println(rg_poa.estaDisponivel(-5) == false);
    // comprando/vendendo passagem
    rg_poa.comprarPassagem();
    System.out.println(rg_poa.estaDisponivel(1) == false);
    System.out.println(rg_poa.estaDisponivel(2) == true);
    System.out.println(rg_poa.quantidadeAssentosDisponiveis() == 24);
    // método sobrecarregado
    rg_poa.comprarPassagem(3);
    System.out.println(rg_poa.quantidadeAssentosDisponiveis() == 23);
    System.out.println(rg_poa.estaDisponivel(1) == false);
    System.out.println(rg_poa.estaDisponivel(2) == true);
    System.out.println(rg_poa.estaDisponivel(3) == false);
    try {
      rg_poa.comprarPassagem(26); // assento não existe
      System.out.println("Esta linha não deve ser impressa");
    } catch (Exception e) {
      System.err.println(e); // assento não existe
    }
    try {
      rg_poa.comprarPassagem(3); // assento indisponível/ocupado
      System.out.println("Esta linha não deve ser impressa");
    } catch (Exception e) {
      System.err.println(e); // assento indisponível/ocupado
    }
    rg_poa.comprarPassagem(5);
    System.out.println(rg_poa.quantidadeAssentosDisponiveis() == 22);
    System.out.println(rg_poa.estaDisponivel(1) == false);
    System.out.println(rg_poa.estaDisponivel(2) == true);
    System.out.println(rg_poa.estaDisponivel(3) == false);
    System.out.println(rg_poa.estaDisponivel(4) == true);
    System.out.println(rg_poa.estaDisponivel(3) == false);
    System.out.println(rg_poa.estaDisponivel(6) == true);
    // compra o primeiro livre
    rg_poa.comprarPassagem();
    System.out.println(rg_poa.quantidadeAssentosDisponiveis() == 21);
    System.out.println(rg_poa.estaDisponivel(1) == false);
    System.out.println(rg_poa.estaDisponivel(2) == false);
    System.out.println(rg_poa.estaDisponivel(3) == false);
    System.out.println(rg_poa.estaDisponivel(4) == true);
    System.out.println(rg_poa.estaDisponivel(3) == false);
    System.out.println(rg_poa.estaDisponivel(6) == true);

  }
}

As várias formas do ticket de pedágio

Considere um ticket de pedágio calculado pelo número de eixos e rodagem. Um ticket pode ser para automóveis, que possuem dois eixos e rodagem simples, ônibus e caminhões com dois eixos e rodagem dupla, e depois combinações de automóveis com reboque de rodagem simples e caminhões com rodagem dupla. Os tickets possuem um valor base, para automóveis, que é escalado de acordo com a quantidade de eixos e rodagem. Seguem os casos de teste:

// App.java
class App {
  public static void main(String[] args) {
    int base = 12;
    Ticket automovel = new Ticket(base, 2, 1); // R$ 12,00, dois eixos, rodagem simples
    System.out.println(automovel.eixos == 2);
    System.out.println(automovel.rodagem == 1);
    // valor base
    System.out.println(automovel.valor() == base); // 12

    Ticket onibus = new Ticket(valor, 2, 2); // R$ 24,00, dois eixos, rodagem dupla
    System.out.println(onibus.eixos == 2);
    System.out.println(onibus.rodagem == 2);
    System.out.println(onibus.valor() == base * 2); // 24
    System.out.println(onibus.valor() == 24); // 24
    // valor base

    Ticket carroComReboque = new Ticket(valor, 3, 1); // R$ 18,00, três eixos, rodagem simples
    System.out.println(carroComReboque.valor() == 18); // base * 2.5

    Ticket rodoTrem = new Ticket(valor, 6, 2);
    System.out.println(rodoTrem.valor() == base * 6); // 72

    // sobrecarregar o construtor para aceitar automóvel como padrão
    Ticket carro = new Ticket(base); // dois eixos, rodagem simples por padrão
    System.out.println(carro.eixos == 2);
    System.out.println(carro.rodagem == 1);
    System.out.println(carro.valor() == base); // 12

    // sobrecarregar o construtor para aceitar a rodagem também textualmente
    Ticket van = new Ticket(base, 2, "dupla"); // dois eixos, rodagem simples por padrão
    System.out.println(van.eixos == 2);
    System.out.println(van.rodagem == 2); // rodagem dupla
    System.out.println(van.valor() == base * 2); // 24

    Ticket camioneta = new Ticket(base, 2, "simples"); // dois eixos, rodagem simples por padrão
    System.out.println(camioneta.eixos == 2);
    System.out.println(camioneta.rodagem == 1); // rodagem simples
    System.out.println(camioneta.valor() == base); // 24

  }
}

Revisitando o Cartão de Crédito para implementar novas formas de compra

Considere alterar o exercício do Cartão de Crédito e sobrecarregar o método comprar para ser invocado sem argumentos indicando um compra à vista.

// App.java
class App {
  public static void main(String[] args) {
    CartaoCredito cartao = new CartaoCredito("DoctorCard", 5000);
    // Testagem
    System.out.println(cartao.operadora.equals("DoctorCard")); // true
    System.out.println(cartao.limiteTotal() == 5000); // true
    System.out.println(cartao.limiteDisponivel() == 5000); // true
    cartao.comprar(10, 100); // compromete 1000 em 10x de 100
    cartao.comprar(1000); // compromete mais 1000, mas em 1x de 1000
    System.out.println(cartao.limiteTotal() == 5000);
    System.out.println(cartao.limiteDisponivel() == 3000);
  }
}

Projeto e implemente seu exemplo

Projete uma classe sobre um domínio de seu interesse que faça uso do polimorfismo ad hoc e escreva testes.