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.